前面我们学习了关于c++语言的一些基础知识,今天我们来正式进入c++语言的学习
一、类的定义
1.1 类的定义
C++是一门面向对象的语言,理解c++,首先要理解类和对象。
C++中的类可以看成c语言中的结构体的升级版,结构体是一种构造类型,可以包含若干个成员变量,成员变量的类型可以不同。而由类声明定义的变量,我们称为对象。
C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是 struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类。
在类里面定义的成员函数默认为内联函数(inline)
1.2 类的语法规格式
class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后⾯分号不能省 略。
class Stack { public: void Init(int n = 4) { array = (int*)malloc(sizeof(int) * n); if (nullptr == array) { perror("malloc申请空间失败"); return; } capacity = n; top = 0; } int* array; size_t capacity; size_t top; };//分号不能省略 int main() { Stack st; st.Init(); return 0; }
1.3 成员变量的命名
虽然C++标准并没有规定成员变量的命名规则,但是大家约定俗成地在定义变量时会有一套特定的规则,目的就是解决可能存在的命名冲突的问题,比如说下面这段代码:
class Date { void Init(int year, int month, int day) { //命名冲突 year = year; month = month; day = day; } int year; int month; int day; };
这时为了解决问题这类问题,我们在定义成员变量时会对其进行特定地修饰。比如说 _变量名 ,m_变量名 和变量名_ 等等不同的修饰
class Date { void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } int _year; int _month; int _day; };
二、限定访问符
在C++类中有三种访问限定符:**public,private,protected。**他们每一个都有自己独特的作用:
- public:公有属性,修饰的成员在类内外都可以直接被访问。
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- private:私有属性
- protected:保护属性
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止。如果后面没有访问限定符,作用域就到 } 即类结束。
那在C++中struct 和class有什么区别呢?
struct :内部默认是公有权限,结构体外部可以访问其内部成员
class:内部默认是私有权限,类的外部不能直接访问内部成员;可以手动声明为共有权限;
三、类域
类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作 ⽤域操作符指明成员属于哪个类域。
#include<iostream> using namespace std; class Stack { public: void Init(int n = 4); int* array; size_t capacity; size_t top; }; void Stack::Init(int n) { array = (int*)malloc(sizeof(int) * n); if (nullptr == array) { perror("malloc申请空间失败"); return; } capacity = n; top = 0; } int main() { Stack st; st.Init(); return 0; }
四、类对象模型
4.1 类对象的实例化
⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。
在类中的成员变量实际是一种声明,相当于一个设计图纸。而我们利用类名定义的对象就是类对象的实例化,相当于通过设计图纸实际创建出来。单独的类是并不占据实际空间的大小。
⼀个类可以实例化出多个对象,实例化出的对象 占⽤实际的物理空间,存储类成员变量。打个比⽅:类实例化出对象就像现实中使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房⼦才能住⼈。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。
4.2 类对象的存储
我们知道了类对象的创建,那么具体类中的成员变量与成员函数又是如何存储的呢?
类实例化出的每个对象,都有独⽴的数据空间,所以对象中肯定包含 成员变量,那么成员函数是否包含呢?
- 每次创建对象时,都开辟一个空间存储类成员变量与成员函数。
- 每次创建对象时,都开辟一个空间存储类成员变量与成员函数的地址。
- 每次创建对象时,都开辟一个空间存储类成员变量。而成员函数提前单独存储一个区域
- ⾸先函数被编译后是⼀段指令,对象中没办法存储,这些指令存储在⼀个单独的区域(代码段),而且每次对象的实例化都会开辟函数的空间。而每个函数的功能都是一样的,这就造成了空间的浪费。那么对象中⾮要存储的话,只能是成员函数的指针。
- 再分析⼀下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各⾃独⽴的成员变量_year/_month/_day存储各⾃的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象中就浪费了,这⾥也就没有必要其。
- 函数指针是⼀个地址,调⽤函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找。
除此之外我们还可以通过计算类的大小来判断是以哪种形式存在的
4.3 类对象的大小
4.3.1 一般类的计算
类型对象的大小我们可以借助运算符sizeof计算,并且类的大小也遵循结构体内存对齐规则,这部分的内容已经在结构体的部分谈过了,就不再赘述,详情见:深入C语言:探索结构体的奥秘 的第五点
#include<iostream> using namespace std; class Date { //可以被直接访问 public: void Init(int year, int month, int day) { _year = year; _month = month; _day = day; } //不能被直接访问 private: int _year; int _month; int _day; }; int main() { Date d; cout << sizeof(d) << endl; return 0; }
根据内存对齐规则,我们知道成员变量的大小就是12,所以证明成员函数是存在于内存的其他位置。所以说存储三正确。一般这个存储成员函数的区域我们称之为公共代码段。
4.3.2 空类的计算
当类中只有成员函数,或者什么都没有时。类的大小又为多少呢?
// 类中仅有成员函数 class A1 { public: void func2() {} }; // 类中什么都没有---空类 class A2 { }; int main() { A1 d1; A2 d2; cout << sizeof(d1) << endl; cout << sizeof(d2) << endl; return 0; }
为什么空类的大小为1,而不是0呢?其实并不能难像,因为在我们进行空类的实例化时必须要有空间存储对象的大小。这是编译器就会默认给一个字节大小来标记这个类的对象,实际操作中实用性也很少。
五、this指针
我们先来看一段代码:
#include<iostream> using namespace std; 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, d2; d1.Init(2022, 1, 11); d2.Init(2022, 1, 12); d1.Print(); d2.Print(); return 0; }
对于上述类,有这样的一个问题:
Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用 Init 函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
那么这⾥就要看到C++给了⼀个隐含的this指针解决这⾥的问题编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个隐藏的当前类类型的指针,叫做this指针,只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。。
void Init(Date* const this, int year, int month, int day) { this->_year = year; this->_month = month; this->_day = day; } d1.Init(&d1,2022, 1, 11);//实际传参
但是注意:我们并不能将隐式传参书写出来,因为这是编译器默认添加的。
虽然我们不能在传参的时候书写this指针,但我们在成员函数内部可以使用this指针,使用this指针的时候需要注意一些问题:
- this指针的类型:类型为const,即成员函数中,不能给this指针赋值。
- 只能在“成员函数”的内部使用。
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
问题:
问题1:
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class Betty { public: void Print() { cout << "Print()" << endl; } private: int _a; }; int main() { Betty* p = nullptr; p->Print(); (*p).Print(); return 0; }
为什么程序会正常运行,对空指针解引用不是会发生运行崩溃吗?首先我们得明白成员函数并不存放在类对象中,而是存放在公共代码段。虽然我们表面看上去解引用,但实际上编译器不需要通过解引用去找对应函数,只需要去公共代码区执行对应函数即可。
问题2:
// 1.下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行 class Betty { public: void Print() { cout << _a << endl; } private: int _a; }; int main() { Betty* p = nullptr; p->Print(); (*p).Print(); return 0; }
这里就引起程序崩溃,因为我们知道访问对应的成员变量,会传递对应对象的地址。cout 需要访问 _a,从而需要对p进行解引用,而这里 p 的地址为 nullptr ,导致了程序的崩溃