什么是面向对象?
c语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
比如洗衣服:
c++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
在C语言中很多的过程在c++中被分为了人 衣服 洗衣机 洗衣粉,想要完成洗衣粉这件事只需要人将衣服放进洗衣机,倒入洗衣粉,启动洗衣机就完成了。
一、类是什么?
C语言结构体中只能定义变量,在c++中结构体内不仅可以定义变量,也可以定义函数。比如:之前我们用C语言方式实现的栈,结构体中只能定义变量,现在以c++的方式实现,会发现struct中也可以定义函数。
struct Stack { void Init(int n = 4) { a = (int*)malloc(sizeof(int) * n); if (nullptr == a) { perror("malloc申请空间失败"); return; } capcity = n; top = 0; } void Push(int x) { } void Pop() { } int Top() { } bool Empty() { } int* a; int capcity; int top; }; int main() { Stack st; st.Init(); st.Push(1); st.Push(2); st.Push(3); return 0; }
就像上面的代码段一样,以前C语言是不支持将函数写入结构体的,而c++现在能做到了。
而上面的struct在c++中更喜欢用class来代替。
那么怎么创建一个类呢?看下面代码段:
class classname // class后面跟你自己想要取的类名 { //类体,由成员函数和成员变量组成 }; //后面一定要加;和结构体一样
class为定义类的关键字,classname为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中的内容称为类的成员,类中的变量称为类的属性或成员变量,类中的函数称为类的方法或者成员函数。类中定义的变量都可以直接在类中使用,类外则需要域限定符。
类的两种定义方式:
1.声明和定义全部放在类体中,需要注意的是,成员函数如果在类中定义,编译器可能会当成内联函数来处理。比如下面这样的:
class classname { public: void add() { year++; } private : int year; };
2.类声明放在头文件中,成员函数的定义放在.cpp文件中。
class classname { public: void add(); private : int year; }; void classname::add() { year++; }
类的访问限定符:
c++中有三种访问限定符,分为public(公有),private(私有)私有的在类外不可以访问,protected(受保护的)同样在类外不可以访问。
c++实现封装的方式:用类将对象的属性和方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
1.pubic修饰的成员在类外可以直接被访问
2.protected和private修饰的成员在类外不能直接被访问
3.访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。
4.如果后面没有访问限定符,作用域到 } 及类结束。
5.class的默认访问权限为private,struct的默认访问权限为public(因为要兼容C语言)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
class Date { public: void Init(int year, int month, int day) { year = year; _month = month; _day = day; } void Print() { cout << year << "年" << _month << "月" << _day << "日" << endl; } private: int year; int _month; int _day; }; int main() { Date d1; d1.Init(2023,2,5); d1.Print(); return 0; }
大家觉得上面这个代码段可以成功打印出年月日吗?答案是不可以,因为我们在private中定义的年与Init函数传来的参数year一样,这就导致编译器识别不出来,所以我们在定义成员变量的时候最好都像month那样在前面加个符号用来区分。
封装 :
面向对象的三大特性:封装,继承,多态。
封装的意思就是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装的本质就是一种管理,让用户更方便的使用类。举个例子:就像电脑的主机一样,主机只提供开机键等接口,而实际上电脑真正工作的东西是cpu等硬件,而这些硬件是不会暴露在外边让用户看到的。
类的实例化:
用类的类型创建对象的过程,称为类的实例化。类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它,比如以下代码:
我们已经说过类是对对象进行描述的,只有创建了一个类对象,才会给这个类对象分配空间,这样就可以使用类里面的函数等变量。下图为正确的使用方式:
下面我们来看类中成员如何存储的:
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; } private: int _year; ///这些只是声明并不是定义 int _month; int _day; }; int main() { Date d1; cout << sizeof(d1) << endl; return 0; }
大家可以猜一猜d1这个对象的大小是多少?答案是12,12不就是private中三个成员变量的大小吗,为什么成员函数不占用空间呢?
大家看上图,d1的year变量和d2的year变量是在同一块空间吗?答案是不在同一块空间,因为对象实例化后会给每个对象都开辟一个空间,那么d1的year肯定是在d1这个对象开辟的空间内,d2的year是在d2这个对象开辟的空间内。
那么 d1的init函数和d2的init函数是在同一块空间吗?答案是是的,c++中为了防止每个对象都开辟空间存储不同的函数所以将函数放在了公共的代码段,想要调用这个函数直接去公共的代码段去找即可,这也就解释了为什么我们再计算对象的大小的时候不包含函数的大小了。至于为什么成员变量不设为公共的问题就很好回答了,应该每个对象都能对自己的成员变量进行修改,如果设为一个公共的那么d2对象修改year的值也会将d1对象的year进行修改。
// 类中既有成员变量,又有成员函数 class A1 { public: void f1() {} private: int _a; }; // 类中仅有成员函数 class A2 { public: void f2() {} }; // 类中什么都没有---空类 class A3 {};
上面这三个类的sizeof大小是多少呢?
有了上面的解释回答这道题就很容易了,首先A1中只有变量_a占实际空间,所以大小为4字节。
A2中只有成员函数,而成员函数在代码段中那么这个A2就相当于A3是一个空类,空类在c++中占一个字节。
this指针
class Date { public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; } private: int _year; ///这些只是声明并不是定义 int _month; int _day; }; int main() { Date d1; Date d2; d1.Init(1, 2, 3); d2.Init(4, 5, 6); d1.Print(); d2.Print(); return 0; }
对于上面的代码段,有这样一个问题:Date类中有Init和Print两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init函数时,该函数是如何知道设置d1对象,而不是设置d2对象呢?
对于这个问题,c++中引用了this指针来解决这个问题。即:c++编译器给每个"非静态的成员函数"增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有"成员变量"的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需用来传递,编译器自动完成。比如下面代码:
class Date { public: void Init(int year, int month, int day)//用户看到的 //实际上 void Init(Date* this,int year,int month,int day) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "年" << _month << "月" << _day << "日" << endl; } private: int _year; ///这些只是声明并不是定义 int _month; int _day; }; int main() { Date d1; Date d2; d1.Init(1, 2, 3);//用户看到的 //实际上 //d1.Init(&d1, 1, 2, 3); d2.Init(4, 5, 6); d1.Print(); d2.Print(); return 0; }
在这里要注意,我们不能显式的自己去调用的时候传入对象的地址,这样编译器会报错。
那么this指针有什么作用呢?
class Date { public: void Init(int year, int month, int day) { this->year = year; this->month = month; this->day = day; cout << this << endl; } void Print() { cout << year << "年" << month << "月" << day << "日" << endl; } private: int year; ///这些只是声明并不是定义 int month; int day; }; int main() { Date d1; Date d2; d1.Init(1, 2, 3); d2.Init(4, 5, 6); d1.Print(); d2.Print(); return 0; }
之前Init函数中不能分辨的year等变量用上this指针就可以正确分辨,还可以打印此对象的地址
那么this指针存放在哪里呢?this指针存放在栈中,因为this是隐含形参/vs下面是存在ecx寄存器中
this指针可以为空吗?看以下代码:
class Date { public: void Init(int year, int month, int day) { this->year = year; this->month = month; this->day = day; cout << this << endl; } void Print() { cout << year << "年" << month << "月" << day << "日" << endl; } void Func() { cout << "Func()" << endl; } private: int year; ///这些只是声明并不是定义 int month; int day; }; int main() { Date* ptr = nullptr; ptr->Func(); return 0; }
上面这段代码可以正常编译吗?很多人看到ptr是个空指针然后去调用Func函数会觉得这里对空指针进行解引用了,这样理解其实是不对的,首先这个代码可以正常编译看下图:
这里解释一下为什么可以编译,我们之前说过调用类中的函数时编译器会隐式修改为传对象的地址然后函数多了一个this指针的参数,所以当我们调用func这个函数的时候,编译器通过this指针找到了类中的这个函数即使把ptr这个空指针传给了this,也是可以正常使用的。那么下面这个程序的运行结果又是怎么样的?
上图这段代码运行起来程序崩溃了,首先这个Init和刚刚的func函数一样都不在对象里面,他们都在公共区域,调用这个函数直接跳到存放代码的地址,这些都没问题,有问题的是ptr是空指针,ptr给this传了个空指针然后再Init函数中这个空指针指向Year这个变量,这就是对空指针进行解引用了。
那么上图中这个代码是否可以正常运行呢?很多人看到括号内对ptr空指针进行解引用了以为程序会崩溃,但其实并不是,编译器还是先去对象里找有没有Func()这个函数,然后编译器发现找不到通过this指针找到了Func函数的公共代码段,而这里的(ptr)是起到了给传给this指针的作用。
那么上图中的这个代码运行起来会不会崩溃呢? 这个一定是崩溃了,编译器先去找year是不是在对象里,找到后发现这个对象有自己的空间所以对空指针进行解引用了。通过上面几个问题大家应该知道this指针是可以为空的了。