文章目录
- 1.类的定义
- 1.1 类定义的格式
- 1.2 访问限定符
- 1.3 类域
- 2.实例化
- 2.2 对象大小
- 3.this指针
- 4.类的默认成员函数
- 5.构造函数
- 6. 析构函数
- 7.拷贝构造函数
1.类的定义
1.1 类定义的格式
- class为定义类的关键字,Stu为类的名字,{}中为类的主体,注意类定义结束时后⾯分号不能省略(与结构体struct一样)。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的⽅法或者成员函数。
- 为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前⾯或者后⾯加_ 或者 m开头,注意C++中这个并不是强制的,只是⼀些惯例,每个地方要求都不一样。
-C++中struct也可以定义类,C++兼容C中struct的⽤法,同时struct升级成了类,明显的变化是struct中可以定义函数,⼀般情况下我们还是推荐⽤class定义类。- 定义在类⾯的成员函数默认为inline。
示例:
class Date
{
public://成员函数或成员方法void Init(int year, int month = 1, int day = 1){_year = year;_month = month;_day = day;}private://成员变量或成员属性int _year;int _month;int _day;
};int main()
{Date d;d.Init(2024, 11, 24);return 0;
}
1.2 访问限定符
- C++实现封装的⽅式,⽤类将对象的属性与⽅法结合在⼀块,让对象更加完善,通过访问权限选择性的将其接⼝提供给外部的⽤⼾使⽤。
- public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。
- 访问权限作⽤域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为⽌,如果后⾯没有访问限定符,作⽤域就到 }即类结束。
- class定义成员没有被访问限定符修饰时默认为private,struct默认为public。
- ⼀般成员变量都会被限制为private/protected,需要给别⼈使⽤的成员函数会放为public。
1.3 类域
- 类定义了⼀个新的作⽤域,类的所有成员都在类的作⽤域中,在类体外定义成员时,需要使⽤ :: 作⽤域操作符指明成员属于哪个类域。
- 类域影响的是编译的查找规则,下⾯程序中Print如果不指定类域Date,那么编译器就把Print当成全局函数,那么编译时,找不到_year等成员的声明/定义在哪⾥,就会报错。指定类域Date,就是知道Print是成员函数,当前域找不到的_year等成员,就会到类域中去查找。
示例:
class Date
{
public://成员函数或成员方法void Init(int year, int month = 1, int day = 1){_year = year;_month = month;_day = day;}//成员方法的声明void Print();private://成员变量或成员属性int _year;int _month;int _day;
};void Date::Print()
{cout << "_year = " << _year << endl;cout << "month = " << _month << endl;cout << "_day = " << _day << endl;
}int main()
{Date d;d.Init(2024, 11, 24);d.Print();return 0;
}
2.实例化
Q:实例化的概念?
A:⽤类类型在物理内存中创建对象的过程,称为类实例化出对象。类是对象进⾏⼀种抽象描述,是⼀个模型⼀样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,⽤类实例化出对象时,才会分配空间。
⼀个类可以实例化出多个对象,实例化出的对象 占⽤实际的物理空间,存储类成员变量。打个⽐⽅:类实例化出对象就像现实中使⽤建筑设计图建造出房⼦,类就像是设计图,设计图规划了有多少个房间,房间⼤⼩功能等,但是并没有实体的建筑存在,也不能住⼈,⽤设计图修建出房⼦,房⼦才能住⼈。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。
示例:
class Date
{
public://成员函数或成员方法void Init(int year, int month = 1, int day = 1){ _year = year;_month = month;_day = day;}//成员方法的声明void Print(){cout << _year << '/' << _month << '/' << _day << endl;}private://成员变量或成员属性//这里只是声明,没有开辟空间int _year;int _month;int _day;
};int main()
{Date d;//实例化对象d.Init(2024, 11, 24);d.Print();return 0;
}
输出: 2024/11/24
2.2 对象大小
分析⼀下类对象中哪些成员呢?类实例化出的每个对象,都有独⽴的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?⾸先函数被编译后是⼀段指令,对象中没办法存储,这些指令存储在⼀个单独的区域(代码段),那么对象中⾮要存储的话,只能是成员函数的指针。再分析⼀下,对象中是否有存储指针的必要呢,Date实例化d1和d2两个对象,d1和d2都有各⾃独⽴的成员变量_year/_month/_day存储各⾃的数据,但是d1和d2的成员函数Init/Print指针却是⼀样的,存储在对象中就浪费了。如果⽤Date实例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这⾥需要再额外哆嗦⼀下,其实函数指针是不需要存储的,函数指针是⼀个地址,调⽤函数被编译成汇编指令[call 地址], 其实编译器在编译链接时,就要找到函数的地址,不是在运⾏时找,只有动态多态是在运⾏时找,就需要存储函数地址,这个我们以后会讲解。
总结上面一段话:class中只用存储成员变量的空间,成员方法是不需要存储的,即他不占用实例化对象的空间。
补充:C++规定类实例化的对象也要符合内存对⻬的规则。(和struct一样)
示例:
class A
{
public:void Print(){cout << "hello C++" << endl;}
private:int a;char c;
};class B
{
public:void Print(){cout << "hello C++" << endl;}
};int main()
{A a;cout << sizeof(a) << endl;B b;cout << sizeof(b) << endl;return 0;
}
输出的结果是8和4。
Q:我们看到没有成员变量的B类对象的⼤⼩是1,为什么没有成员变量还要给1个字节呢?
A:因为如果⼀个字节都不给,怎么表⽰对象存在过呢!所以这⾥给1字节,纯粹是为了占位标识对象存在。
3.this指针
同样上面Date的例子:
- Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调⽤Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这⾥就要看到C++给了⼀个隐含的this指针解决这⾥的问题
- 编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this指针。⽐如Date类的Init的真实原型为:
void Init(Date* const this, int year, int month, int day)
- 类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值
- C++规定不能在实参和形参的位置显⽰的写this指针(编译时编译器会自己处理),但是可以在函数体内显⽰使⽤this指针。
分享几道题目;
1.下⾯程序编译运⾏结果是()
A、编译报错 B、运⾏崩溃 C、正常运⾏
class A
{
public:void Print(){cout << "A::Print()" << endl;}
private:int _a;
};int main()
{A* p = nullptr;p->Print();return 0;
}
正确答案应该是正常运行。首先空指针不会报编译错误!!!空指针是一个语法错误。
之前我们说过,class只会存放成员变量,不会存成员函数 ,也不会存成员函数的地址,在执行 p->Print()的时候,会直接call Print()。虽然会有空指针的存在,但是我们不会对空指针进行解引用的操作。
2.下⾯程序编译运⾏结果是()
A、编译报错 B、运⾏崩溃 C、正常运⾏
#include<iostream>
using namespace std;
class A
{
public:void Print(){cout << "A::Print()" << endl;cout << _a << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->Print();return 0;
}
这题应该是运行崩溃。在执行cout << _a << endl;
这句话时,程序会自动转换成cout << p->_a << endl;
这里对空指针进行解引用了,运行崩溃。
3.this指针存在内存哪个区域的 ()
A. 栈 B.堆 C.静态区 D.常量区 E.对象⾥⾯
这题应该是栈。
4.类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后⾯再讲解。默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯去学习:
- 第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
- 第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?
5.构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),⽽是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数⾃动调⽤的特点就完美的替代的了Init。
构造函数的特点:
- 函数名与类名相同。
- ⽆返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
- 对象实例化时系统会⾃动调⽤对应的构造函数。
- 构造函数可以重载。
- 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦用户显式定义编译器将不再⽣成。
- ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造,总结⼀下就是不传实参就可以调⽤的构造就叫默认构造。
- 我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。对于⾃定义类型成员变量,要求调⽤这个成员变量的默认构造函数初始化。(比如说MyQueen就不需要写构造函数,我们会调用Stack的默认构造函数) 如果这个成员变量,没有默认构造函数,那么就会报错 ,我们要初始化这个成员变量,需要⽤初始化列表才能解决,初始化列表,我们下个章节再细细讲解。
注意:C++把类型分为内置类型(基本类型)和自定义类型。内置类型就是语言提供的原生数据类型,例如int/char/double/指针等等;自定义类型就是我们使用class/struct等关键字自己定义的类型。
示例:
class Date
{
public://1.无参构造函数(默认构造函数)Date(){_year = 1;_month = 1;_day = 1;}//2.带参构造函数Date(int year, int month, int day){this->_year = year;this->_month = month;this->_day = day;}//3.全缺省构造函数(也是默认构造函数)//注意:三个默认构造函数只能同时存在一个/*Date(int year = 1, int month = 1, int day = 1){this->_year = year;this->_month = month;this->_day = day;}*/void Print(){cout << _year << '/' << _month << '/' << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1; //实例化对象时,会调用他的默认构造函数Date d2(2024, 11, 24);//调用带参构造函数d1.Print();d2.Print();return 0;
}
6. 析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的,函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对象中资源的清理释放⼯作。析构函数的功能类⽐我们之前Stack实现的Destroy功能,⽽像Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
析构函数的特点:
- 析构函数名是在类名前加上字符 ~。
- ⽆参数⽆返回值。 (这⾥跟构造类似,也不需要加void)
- ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
- 对象⽣命周期结束时,系统会⾃动调⽤析构函数。
- 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
- 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要⾃⼰写析构,否则会造成资源泄漏,如Stack。
- ⼀个局部域的多个对象,C++规定后定义的先析构。
7.拷贝构造函数
如果⼀个构造函数的第⼀个参数是⾃⾝类类型的引⽤,且任何额外的参数都有默认值,则此构造函数也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。
拷贝构造函数的特点:
- 拷⻉构造函数是构造函数的⼀个重载。
- 拷⻉构造函数的参数只有⼀个且必须是类类型对象的引⽤,使⽤传值⽅式编译器直接报错,因为语法逻辑上会引发⽆穷递归调⽤。
- C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值返回都会调⽤拷⻉构造完成。
- 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。⾃动⽣成的拷⻉构造对内置类型成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉),这里会有很大的危险,尽量不要采用这种方式。比如我们手写栈的时候,采用默认的拷贝构造,最后我们会析构两次(报错),因为我们两个stack的数组共用一块内存的。对⾃定义类型成员变量会调⽤他的拷⻉构造。
- 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现MyQueue的拷⻉构造。这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就需要显⽰写拷⻉构造,否则就不需要。
- 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),没有产⽣拷⻉。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使⽤引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。传引⽤返回可以减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。
注意:C语言中的struct只能完成浅拷贝(C语言中的一个坑)
无限递归:
在这里举例子太乱了,建议自己手动敲敲这些代码!!!我们要注意传值传参和传值返回class对象时会调用拷贝构造。