类和对象

  • 一、类和对象基本知识点
    • 1.1 类的基本概念
    • 1.2 类的定义
    • 1.3 类的访问限定符
    • 1.4 封装
    • 1.5 类对象的大小
    • 1.6 this指针
      • 1.6.1 类的初始化
      • 1.6.2 this指针的特性
  • 二、类的六个默认成员函数
    • ① 构造函数
    • ② 析构函数
    • ③ 拷贝构造函数
    • ④ 赋值运算符重载函数
      • Ⅰ 运算符重载
      • Ⅱ 赋值运算符重载
    • const修饰类的成员函数
    • ⑤和⑥ 取地址及const取地址操作符重载
  • 三、类和对象的知识点进阶
    • 3.1 再谈构造函数
      • 3.1.1 初始化列表
      • 3.1.2 explicit关键字
    • 3.2 static成员
      • 3.2.1 static成员对象
      • 3.2.2 static成员函数
    • 3.3 C++11的成员初始化
    • 3.4 友元
      • 3.4.1 友元函数
      • 3.4.2 友元类
    • 3.5 内部类

一、类和对象基本知识点

1.1 类的基本概念

首先要知道类和C语言的结构体很像,都是定义出一个新的类型,结构体只能定义变量,而类不仅可以定义变量,还可以定义函数

要注意的是在C++中struct兼容C语言的所有用法,同时把struct升级为类,C++用class定义类。


1.2 类的定义

class className{ // 类体:由成员函数和成员变量组成}; // 一定要注意后面的分号

class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。
类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。

类的定义也有两种方式:
1️⃣声明和定义全部放在类中

class Person{public://打印基本信息void Print(){cout << _name << "-" << _sex << "-" << _age << endl;}private:char* _name;char* _sex;int _age;};

2️⃣声明和定义分开

//.hclass Person{public://打印基本信息void Print();private:char* _name;char* _sex;int _age;};
//.cppvoid Person::Print(){cout << _name << "-" << _sex << "-" << _age << endl;}

要注意一点:{}说明是个域,要用::访问


1.3 类的访问限定符


说明:

暂时把protectedprivate看成一样的,public就是在类外面能直接被访问,而private在类外面不能被直接访问
当不写的时候class的默认访问权限为private,struct为public(因为struct要兼容C)

【面试题】
C++中struct和class的区别是什么?

C++需要兼容C语言,所以C++中struct可以当成结构体去使用。另外C++中struct还可以用来定义类。
和class是定义类是一样的,区别是struct的成员默认访问方式是public,class是struct的成员默认访问方式
是private。


1.4 封装

面向对象的三大特性:封装、继承、多态。
封装:类和对象都封装到类中,想被访问的定义成公有,不想被访问的定义成私有。
封装的本质是管理
封装更加严格,规范,不封装更自由,对使用的人素养要求高。


1.5 类对象的大小

来看一个类:

class Date{public:void Print(){cout << _year << _month << _day << endl;}private:int _year;int _month;int _day;};int main(){Date d;cout << sizeof(d) << endl;return 0;}

最后编译的结果是 12
我们可以看到光private内的三个变量就达到了12,那么类中的成员函数为什么不算在内?

假设我们实例化了两个对象d1,d2,他们的三个成员变量不一定相同,但是他们的Print函数却相同,如果给每个对象都在内存中存函数的地址,势必会造成空间浪费。所以在计算大小时不管成员函数

计算成员变量的大小也遵循内存对齐规则结构体/联合体大小的计算

用一张图理解一下:

特殊情况:

class A{//类中仅有成员函数public:void f() {}};//空类class B{};int main(){A a;B b;cout << sizeof(a) << endl;cout << sizeof(b) << endl;return 0;}

两个的结果都为 1
为什么不是0?

给了一个字节不是为了存储数据,是为了占位,表示对象存在过

不然无法区分B b; B bb; B bbb;这三个对象。当我们看这三个对象的地址会发现他们不一样。


1.6 this指针

1.6.1 类的初始化

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 d1;d1.Init(2022, 1, 1);Date d2;d2.Init(2022, 1, 2);return 0;}

我们知道d1和d2两个对象初始化调用和的都是同一个函数,那么为什么调用的同一个函数,却能完成各自的初始化呢?

1.6.2 this指针的特性

看似init函数有三个参数,而编译器会增加一个隐含参数

Init(Date* this, int year, int month, int day)

init内部可以写成:

void Init(int year, int month, int day){this->_year = year;this->_month = month;this->_day = day;}

那么在主函数内:

Date d1;d1.Init(2022, 1, 1);//d1.Init(&d1, 2022, 1, 1)Date d2;d2.Init(2022, 1, 2);//d2.Init(&d2, 2022, 1, 2)

this指针特性:

1️⃣this指针是隐含的,编译时增加的,不能自己在函数的调用和定义中加
2️⃣可以在成员函数中使用

【面试题】
下面程序能运行通过吗?

class A{public:void PrintA(){cout << _a << endl;}void Show(){cout << "Show()" << endl;}private:int _a;};int main(){A* p = nullptr;p->PrintA();p->Show();}

答案是p->PrintA()不能运行成功(空指针),而p->Show()可以
p->PrintA()可以看作p->PrintA(&p),而题中函数的_a可以看作this->a,就是空指针解引用。而p->Show()不会出出现这种情况


二、类的六个默认成员函数

先定义出个日期类:

class Date{public:private:int _year;int _month;int _day;};

类里面的成员函数我们什么都不写,编译器也会默认生成6个函数,这六个函数叫做默认(缺省)成员函数

① 构造函数

构造函数完成的是对象的初始化
构造函数的存在是为了防止我们忘了使用自己定义的初始化函数,而直接去使用对象发生错误
特性:

1️⃣ 函数名与类名相同。
2️⃣ 无返回值。
3️⃣ 对象实例化时编译器自动调用对应的构造函数。

class Date{public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;};

当我们对象实例化后它会自动调用Date函数

4️⃣ 构造函数可以重载。
5️⃣ 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参
构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。

能够重载说明了有多种初始化方式

class Date{public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}Date(){_year = 2022;_month = 1;_day = 1;}private:int _year;int _month;int _day;};int main(){Date d1(2022, 7, 9);Date d2;return 0;}

d1 调用的是有参数的构造函数,d2 调用的是无参的构造函数。
而我们可以把两个重载函数合二为一,写成全缺省参数。

Date(int year = 0, int month = 1, int day = 1){_year = year;_month = month;_day = day;}

要注意一种情况:
当有参数的构造函数写成全缺省的时候,d2实例化就会出错,虽然语法上没什么问题,但是编译器却不知道该调哪个函数。

6️⃣ 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

class Date{public:void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;}private:int _year;int _month;int _day;};int main(){Date d1;d1.Print();return 0;}

输出结果:

-858993460年-858993460月-858993460日

我们可以看到,好像编译器默认生成的构造函数好像什么事都没做

class A{public:A(){_a = 0;}void Print(){cout << _a << endl;}private:int _a;};class Date{public:void Print(){cout << _year << "年" << _month << "月" << _day << "日" << endl;_aa.Print();}private:int _year;int _month;int _day;A _aa;};int main(){Date d1;d1.Print();return 0;}

这里编译器把_aa的值初始化为0。所以得出结论:

编译器默认生成的构造函数,对内置类型(如int,char,指针等等)默认不初始化,对自定义类型(如class,struct)编译器会调用他们的默认构造函数初始化。

默认构造函数:
有很多人认为默认构造函数就是我们没写,编译器默认生成的函数,这种理解是不全面的

1️⃣ 我们不写,编译器默认生成的
2️⃣ 我们自己写的无参的
3️⃣ 我们自己写的全缺省的

用一句话来说就是:不用参数就可以调用的构造函数

总结:

默认函数大多数都是要自己写的,并且建议写成全缺省的形式


② 析构函数

首先明白一点:析构函数不是完成对象的销毁,对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
特性:

1️⃣ 析构函数名是在类名前加上字符 ~。
2️⃣ 无参数无返回值。
3️⃣ 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
4️⃣ 对象生命周期结束时,C++编译系统系统自动调用析构函数。

class Date{public:~Date(){cout << "~Date()" << endl;}private:int _year;int _month;int _day;};int main(){Date d1;return 0;}

其实在日期类里面,析构函数没有起到什么作用,写不写都一样,那么在什么情况下析构函数有用呢?

class Stack{public:Stack(int capacity = 4){_a = (int*)malloc(sizeof(int) * capacity); _size = 0;_capacity = capacity;}~Stack(){free(_a);_a = nullptr;_size = _capacity = 0;}private:int* _a;int _size;int _capacity;};

我们在数据结构中学到的栈,每次都要写Destroy函数,而在C++中我们就不用自己写了。像Stack类析构函数具有重大意义。

5️⃣ 先构造的后析构,后构造的先析构

int main(){Stack s1;stack s2;return 0;}

对象是定义在栈中,函数调用会建立栈帧,栈帧中的对象构造和析构也符合后进先出的规则。
s1先构造
s2后构造
s1后析构
s2先析构

析构函数的跟构造函数类似,对内置类型和自定义类型处理方式不同,内置类型不处理,自定义类型会处理。
总结一下:

当你的类在构造函数执行过程中申请了一些资源(内容空间),需要在对象被销毁时进行释放时,就需要自己定义析构函数。


③ 拷贝构造函数

拷贝构造就跟字面意思一样,比方说实例化了一个日期类对象d1,现在想要实例化个跟d1信息一样的对象,就要用到拷贝构造。

int main(){Date d1(2022, 7, 9);Date d2(d1);return 0;}

那么我们就来看看拷贝构造怎么实现?
特性:

1️⃣ 拷贝构造函数是构造函数的一个重载形式

Date(Date d){_year = d._year;_month = d._month;_day = d._day;}

验证发现这个代码编不过,这就要看第二个特性:

2️⃣ 拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。

为什么上面的函数会无穷递归呢?

而如果用引用:

从次可得:
函数传参,如果传的是自定义类型的对象,推荐使用传引用。如果使用传值,就会调用拷贝构造。

Date(const Date& d){_year = d._year;_month = d._month;_day = d._day;}

3️⃣ 若未显示定义,系统生成默认的拷贝构造函数。它会按照字节序拷贝,我们称作浅拷贝(值拷贝),就是对内置类型浅拷贝,

但是浅拷贝又会在一些情况下出现问题,比如Stack类。

总结一下:

拷贝构造函数是特殊的构造函数,涉及到深浅拷贝,像Date这样的类就需要浅拷贝,不需要自己写,但是像Stack这种的类就需要自己写,不然会导致对象析构两次,导致崩溃。


④ 赋值运算符重载函数

Ⅰ 运算符重载

C++为了增强代码的可读性引入了运算符重载,而赋值运算符重载就是运算符重载中的一种。
在以前我们如果要判断两个日期对象是否相等时:

class Date{public:Date(int year = 0, int month = 1, int day = 1){_year = year;_month = month;_day = day;}bool Equal(const Date& d1){return this->_day == d1._day&& this->_month == d1._month&& this->_year == d1._year;}private:int _year;int _month;int _day;};int main(){Date d1(2022, 7, 9);Date d2(2022, 7, 9);cout << d1.Equal(d2) << endl;return 0;}

这样我们就面临一个问题:代码可读性差
如果我们能写成d1 == d2是不是就能通俗易懂了呢?
C++就可以用运算符重载来实现,使用关键字operator

//定义在全局,注意把成员变量设成公有bool operator==(const Date& d1, const Date& d2){return d1._day == d2._month&& d1._month == d2._month&& d1._year == d2._year;}

而我们比较时就有两种写法(两种一样):

operator==(d1, d2);d1 == d2;

但是如果我们不想变成公有呢?
那么就写成成员函数:

bool operator==(const Date& d1){return this->_day == d1._day&& this->_month == d1._month&& this->_year == d1._year;}

比较用法:

d1.operator==(d2);//d1.operator(&d1, d2);d1 == d2;//d1.opreator(&d1, d2);

其他运算符重载方法相同,例如+ - * /

注意:

1️⃣ 不能通过连接其他符号来创建新的操作符:比如operator@
2️⃣ 重载操作符必须有一个类类型或者枚举类型的操作数
3️⃣ 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不 能改变其含义
4️⃣ 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的操作符有一个默认的形参this,限定为第一个形参
5️⃣ .*::sizeof" />Date d1(2022, 7, 9);Date d2;d2 = d1;

这样就可以完成拷贝赋值
那么我们怎么能自己实现呢?

void operator=(const Date& d){this->_day = d._day;this->_month = d._month;this->_year = d._year;}

但是这么实现不太好,因为无法连续赋值(d1 = d2 = d3)。

//返回值不加&又会调拷贝构造Date& operator=(const Date& d){this->_day = d._day;this->_month = d._month;this->_year = d._year;return *this;}

总结一下:

编译器默认生成的赋值运算符跟拷贝构造函数的特性一样,就是说像Date这种类我们自己不用写。对自定义类型,会去调他的赋值运算符重载完成拷贝。


const修饰类的成员函数

将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改
假如我们想调用比较是否相等的运算符重载函数但是写错了:

class Date{public:Date(int year = 0, int month = 1, int day = 1){_year = year;_month = month;_day = day;}bool operator==(const Date& d){return (this->_day = d._day)&& (this->_month = d._month)&& (this->_year = d._year);}private:int _year;int _month;int _day;};int main(){Date d1(2022, 7, 10);Date d2(2022, 7, 10);d1 == d2;return 0;}

就变成了赋值运算符重载了。那么我们有没有什么方法可以避免呢?
我们可以给this指针也加上const
因为this是隐含的,所以就有以下这种方法:

bool operator==(const Date& d) const//修饰*this{return (this->_day = d._day)//error&& (this->_month = d._month)//error&& (this->_year = d._year);//error}

const权限问题:

  1. const对象可以调用非const成员函数吗?
  2. 非const对象可以调用const成员函数吗?
  3. const成员函数内可以调用其它的非const成员函数吗?
  4. 非const成员函数内可以调用其它的const成员函数吗?

1 不可以
2 可以

前两个好理解,重要的是后面两个。
3 不可以

void Fun() const{Print();}void Print(){cout << _year << endl;}

这里可以看成Fun的this传递给Print(),权限放大,不可以。

4 可以

void Fun() {Print();}void Print() const{cout << _year << endl;}

这个实际上是Fun的this传递给Print(),权限缩小,可以。

这里本质就是要看指针的传递。谁调用就是谁传递给别人。


⑤和⑥ 取地址及const取地址操作符重载

这两个操作符不重要,了解就行。
取地址操作符重载:

Date* operator&(){return this;}

const取地址操作符重载:

const Date* operator&() const{return this;}

三、类和对象的知识点进阶

3.1 再谈构造函数

3.1.1 初始化列表

像我们前面的构造函数初始化使用的是函数体内初始化
初始化列表初始化也可以完成初始化。

Date(int year = 0, int month = 1, int day = 1): _year(year), _month(month), _day(day){}

以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个”成员变量”后面跟一个放在括号中的初始值或表达式,不要忘记最后的{}。

使用的时候两种方法都可以使用,并且可以混着使用:

Date(int year = 0, int month = 1, int day = 1): _year(year), _month(month){_day = day;}

注意:

1️⃣ 每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
2️⃣ 类中包含以下成员,必须放在初始化列表位置进行初始化

① 引用成员变量
② const成员变量
③ 自定义类型成员(该类没有默认构造函数)

class A{public://不是默认构造函数(需要传参)A(int x): _x(x){}private:int _x;};class Date{public:Date(int year = 1): _year(year), _n(1), _p(year), _a(0) // 显示调用{//不能在函数体内初始化,只能使用列表初始化//_n = 1; //_p = year;}private:int _year;//引用必须在定义时初始化,但此处是声明,所以没问题int& _p;const int _n;A _a;};

3️⃣建议尽量使用初始化列表初始化,因为初始化列表是对象定义的地方,就算不写也会使用初始化列表初始化(随机值)。
4️⃣ 成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

class A{public:A(int a):_a1(a), _a2(_a1){}void Print() {cout << _a1 << " " << _a2 << endl;}private:int _a2;int _a1;};int main() {A aa(1);aa.Print();return 0;}

成员变量初始化顺序是按照声明的顺序走的,先初始化_a2,但此时_a1还没被初始化,所以_a2是随机值。然后用a初始化_a1,_a1的值为1。

补充一个知识点:
有时候我们会看到A(3);这种构造方式,他是构造匿名对象,它的生命周期只在这一行中,这一行走完就会调用析构函数。

那么什么情景下会用到匿名对象呢?

要使用一个对象,但只在这一行有用,就可以用匿名对象,方便快捷。

3.1.2 explicit关键字

单参数的构造函数,支持隐式类型转换。

class A{public:A(int a): _a(a){}private:int _a;};int main(){A a = 2;//相当于先构造一个临时变量 A tmp(2); //再拷贝构造 A a(tmp);return 0;}

而现在的编译器进行了优化,相当于直接调构造函数:A a(2)
但是如果我们不想让它隐式类型转换,就可以用explicit关键字:

class A{public:explicit A(int a): _a(a){}private:int _a;};

如果再想A a = 2; 就会报错
用explicit修饰构造函数,将会禁止单参构造函数的隐式转换。


3.2 static成员

3.2.1 static成员对象

class A{public:private:static int _n;};int main(){cout << sizeof(A) << endl;return 0;}

这个的结果为 1,说明_n是存在静态区的,属于整个类,也属于类的所有对象。

使用场景:
计算程序中A定义出了多少个对象?

class A{public:A(){++_n;}A(const A& a){++_n;}//所有的对象都是构造或者拷贝构造出来的!!int GetCount(){return _n;}private://要注意这里是声明,他的初始化要在类外面。static int _n;};int A::_n = 0;int main(){A a1;A a2;A a3;cout << a1.GetCount() << endl;return 0;}

3.2.2 static成员函数

static int GetCount(){return _n;}

他跟成员函数的区别是没有this指针不能访问非静态成员

静态成员和静态成员函数都不属于某个对象,突破类域就能访问(也要看访问限定符)。

总结:

1️⃣ 静态成员为所有类对象所共享,不属于某个具体的对象
2️⃣ 静态成员变量必须在类外定义,定义时不添加static关键字
3️⃣ 类静态成员即可用类名::静态成员或者对象.静态成员来访问
4️⃣ 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
5️⃣ 静态成员和类的普通成员一样,也有public、protected、private3种访问级别,也可以具有返回值


3.3 C++11的成员初始化

我们知道编译器生成的默认构造函数对内置类型默认不初始化,所以在C++11中引入了解决办法:

class A{public:private:int _a = 0;int* _p = nullptr;};

我们没有写构造函数,内置类型也会初始化。要注意的是:
这里不是初始化,依旧是声明,0 和 nullptr为缺省值,如果我们自己写构造函数(不是默认构造函数),没给参数就用缺省值初始化。


3.4 友元

3.4.1 友元函数

我们发现前面调用Print()函数的操作比较复杂,那么我们能不能使用operator<<来让我们输出?
查C++库可以知道cout的类型是ostream

void operator<<(ostream& out){out << _year << "-" << _month << "-" << _day << endl;}

但是当我们用cout << d1调用时候却发现出现错误。

//cout << d1//第一个参数是左操作数,第二个是右操作数,两个操作数反了!!void operator<<(ostream& out)//void operator<<(Date* this, ostream& out)

所以必须写成 d1 << cout,但是不能满足我们的需求。
所以我们要写到全局才能调整顺序。

//全局void operator<<(ostream& out, const Date& d){out << d._year << "-" << d._month << "-" << d._day << endl;}

但是写到全局又会出现访问权限的问题。
这里就引入友元函数:

class Date{//友元函数的声明//返回值为ostream是为了连续输出friend ostream& operator<<(ostream& out, const Date& d);public:Date(int year = 0, int month = 1, int day = 1){_year = year;_month = month;_day = day;}private:int _year;int _month;int _day;};ostream& operator<<(ostream& out, const Date& d){out << d._year << "-" << d._month << "-" << d._day << endl;return out;}

说明:

1️⃣ 友元函数可访问类的私有和保护成员,但不是类的成员函数(无this指针)
2️⃣ 友元函数不能用const修饰(无this指针)
3️⃣ 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
4️⃣ 一个函数可以是多个类的友元函数
5️⃣ 友元函数的调用与普通函数的调用和原理相同

3.4.2 友元类

友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员

class Date;//前置声明class Time{public:Time(int hour = 0, int minute = 0, int second = 1){_hour = hour;_minute = minute;_second = second;}private:int _hour;int _minute;int _second;};class Date{public:Date(int year = 0, int month = 1, int day = 1){_year = year;_month = month;_day = day;}void PrintTime(){// 直接访问时间类私有的成员变量cout << _t._hour << endl;}private:int _year;int _month;int _day;Time _t;};

这里我们想在Date类里面调用PrintTime()函数去访问时间类的私有成员变量,明显不可行,但是当我们让Date变成Time的友元类后就可以了。

class Time{// 声明日期类为时间类的友元类,//则在日期类中就直接访问Time类中的私有成员变量friend class Date; public:Time(int hour = 0, int minute = 0, int second = 1){_hour = hour;_minute = minute;_second = second;}private:int _hour;int _minute;int _second;};

注意:

1️⃣ 友元关系具有单向性,没有交换性。
Date是Time的友元类,那么就可以在Date中直接访问Time的私有成员。但是Time却不能访问Date。
2️⃣ 友元关系不能传递。
如果B是A的友元,C是B的友元,则不能说明C时A的友元。

一般情况下不建议使用友元,因为会破坏封装。


3.5 内部类

就是在一个类的里面再定义一个类。
而内部的类天生是外部的类的友元。

class A{public://B叫做A的内部类class B//B天生是A的友元{private:int _b;};private:int _a = 0;int* _p = nullptr;};

B可以直接访问A。
特点:

1️⃣ 内部类可以定义在外部类的public、protected、private都是可以的。
2️⃣ 注意内部类可以直接访问外部类中的static、枚举成员,不需要
外部类的对象/类名。
3️⃣ sizeof(外部类)=外部类,和内部类没有任何关系