目录
面向过程和面向对象的初步认识
- C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
- C++是基于面向对象的,关注的是对象,将一件事拆分成不同的对象,靠对象之间的交互完成。
我们就 洗衣服 来看看面向过程和面向对象之间的区别:
面向过程,我们的关注点应该是下面的过程

面向对象,那我们的关注点应该就是人、衣服以及洗衣粉和洗衣机这四个类对象之间的关系。

类的引入
- 在C语言中,结构体中只能定义变量
- 在C++中,结构体内不仅可以定义变量,也可以定义函数。
- 比如: 之前在C语言中,用C语言方式实现的栈,结构体中只能定义变量;现在以C++方式实现会发现struct中也可以定义函数。
- struct和class用法差不多(struct 的默认访问限定符是 public,class 的默认访问限定符是 private ),但是在C++中更偏向于用 class。
struct Test
{
// 成员变量
int a;
int b;
// 成员函数
int Add(int x, int y)
{
return x + y;
}
};
类的定义
class className
{
// 类体:由成员函数和成员变量组成
};
- class 为定义类的关键字
- ClassName 为类的名字
- { } 中为类的主体,注意类定义结束时后面分号不能省略
- 类体中内容称为类的成员
- 类中的变量称为类的属性或成员变量
- 类中的函数称为类的方法或者成员函数
类的两种定义方式
一、声明和定义全部放在类体中

二、声明放在头文件(.h)中,定义放在源文件(.cpp)中

一般情况下,更期望采用第二种方式(声明和定义分离)
类的访问限定符
C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
访问限定符的特性
- public(公有) 修饰的成员在类外可以直接被访问
- protected(保护) 和 private(私有) 修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为 private(私有),struct为 public(公有)(因为struct要兼容C)
类的作用域和实例化
- 类定义了一个新的作用域,类的所有成员都在类的作用域中
- 在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域
class Person
{
public:
// 打印信息
void show();
private:
std::string _name; // 名字
std::string _sex; // 性别
int age; // 年龄
};
void Person::show()
{
std::cout << _name << "-" << _sex << "-" << age << std::endl;
}
用类类型创建对象的过程,称为类的实例化
- 类是对 对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
- 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间存储类成员变量
- 类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间.

类对象的大小以及储存方式
一个类当中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?类的大小又是如何计算的呢?
class Person
{
public:
// 打印信息
void show();
private:
std::string _name; // 名字
std::string _sex; // 性别
int age; // 年龄
};
类对象的储存方式我们有如下两种猜测
猜测一:对象中包含类的各个成员

每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同的代码保存了多次,浪费很多空间
猜测二:只保存成员变量,成员函数存放在公共的代码段
每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象需要使用函数时,去代码公共区调用即可
我们来做一个小实验,验证一下
class A1
{
public:
void f1(){}
private:
int _a;
};
// 类中仅有成员函数
class A2
{
public:
void f2() {}
};
// 类中什么都没有(空类)
class A3
{};
我们通过 sizeof 来获取这三个对象的大小
- 我们可以得出一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐
- 注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
类对象的储存方式:只保存成员变量,成员函数存放在公共的代码段 。我们打个比方,类就相当于是一个小区,成员变量就相当于各家各户的房间。成员函数相当于公共的游泳池,健身房,乒乓球桌,娱乐设施。
内存对齐规则
- 第一个成员在与类偏移量为0的地址处
- 其他成员变量要对齐到对齐数的整数倍的地址处。
- 对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。
- Visual Studio(编译器)中默认的对齐数值为 8
- 在 gcc 中没有默认的值,那么对齐数就是成员自身的大小
- 类的总大小为最大对齐数的整数倍。如果不够则浪费空间对齐
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
若有不明白内存对齐:【C++】:内存对齐_c语言结构体内存对齐-优快云博客
this指针
我们先做一个简单的小实验
我们先来定义一个日期类 Date
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指针解决该问题,即:C++编译器给每个“非静态的成员函数“ 增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
对象d1和d2调用成员函数传参时,实际上还传入了指向该对象的指针
编译器进行编译时,看到的成员函数实际上也和我们所看到的不一样,每个成员函数的第一个形参实际上是一个隐含的this指针,该指针用于接收调用函数的对象的地址,用this指针就可以很好地访问到该对象中的成员变量

- this指针的类型:指向类类型的指针 ,即成员函数中,不能给this指针赋值
- 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过 ecx寄存器自动传 递,不需要用户传递
类的6个默认成员函数
class A {}; // 空类
- 构造函数
- 析构函数
- 拷贝构造函数
- 赋值运算符重载
- const成员函数
- 取地址及const取地址操作符重载
构造函数
概念
- 构造函数是一个特殊的成员函数,名字与类名相同
- 创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值
- 并且在对象整个生命周期内只调用一次
class Date
{
public:
// 构造函数
Date(){ }
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象
特性
- 函数名与类名相同
- 无返回值(这里所说的构造函数无返回值是真的无返回值,而不是说返回值为void )
- 对象实例化时编译器自动调用对应的构造函数(创建类类型对象时由编译器自动调用对应的构造函数)
- 构造函数可以重载(我们可以有多种初始化对象的方式,编译器会根据你所传递的参数去调用对应的构造函数)
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个,默认构造函数有如下三种:
- 无参构造函数
- 全缺省构造函数
- 我们没写编译器默认生成的构造函数
- 如果类中没有显式定义构造函数,则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;
}

但是看起来编译器生成的默认构造函数又没什么用?d1对象 调用了编译器生成的默认构造函数,但是d1对象的成员变量依旧是随机值。也就说编译器生成的默认构造函数并没有什么用?
我们来做一个小实验
class Person
{
public:
Person()
{
cout << "调用了构造函数" << endl;
}
};
class Date
{
public:
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
Person wh;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
C++把类型分成内置类型(基本类型)和自定义类型
- 内置类型就是语言提供的数据类型,如:int/char 等等
- 就是我们使用 class/struct/union 等自己定义的类型
看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员wh 调用的它的默认成员函数
- 编译器自动生成的构造函数对内置类型不做处理。
- 对于自定义类型,编译器会再去调用它们自己的默认构造函数。
注意:虽然在我们不写的情况下,编译器会自动生成构造函数,但是编译器自动生成的构造函数可能达不到我们想要的效果,所以大多数情况下都需要我们自己写构造函数
析构函数
概念
- 析构函数:与构造函数功能相反,析构函数负责完成对象的销毁,对象在销毁时会自动调用析构函数,完成类的一些资源清理工作。
- 我们知道当一个类对象销毁时,其中的局部变量也会随着该对象的销毁而销毁,例如,我们用日期类创建了一个对象d1,当d1被销毁时,对象d1当中的局部变量_year/_month/_day也会被编译器销毁。
- 但是这并不意味着析构函数没有什么意义。像栈(stack)这样的类对象,当该对象被销毁时,其中动态开辟的栈并不会随之被销毁,需要我们对其进行空间释放,这时析构函数的意义就体现了。
class Date
{
public:
// 构造函数
Date() {}
// 析构函数
~Date() {}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
特性
- 析构函数名是在类名前加上字符 ~
- 无参数无返回值类型(析构函数所谓的无返回值也是真的无返回值,而不是返回值为void)
- 一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数
- 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类
- 先构造的后析构,后构造的先析构
- 因为对象是定义在函数中的,函数调用会建立栈帧,栈帧中的对象构造和析构也要符合先进后出的原则

注意:我们在编写代码的时候,最好还是写上析构函数
拷贝构造函数
概念
- 拷贝构造函数是一种特殊的构造函数,用于创建一个新对象,并用另一个同类型的已有对象来初始化这个新对象(说明这个新对象先是不存在的)
- 拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存 在的类类型对象创建新对象时由编译器自动调用。
class Date
{
public:
// 构造函数
Date(int year = 2024, int month = 6, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
cout << "拷贝构造函数" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
// 析构函数
~Date()
{
cout << "~Date()" << endl;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
特性
- 拷贝构造函数是构造函数的一个重载形式( 重载是指在同一个作用域内,可以有多个具有相同名称但参数列表不同的函数或方法)
- 拷贝构造函数的参数只有一个且必须是引用传参,使用传值方式编译器直接报错,因为会引发无穷递归调用
- 若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
- 在编译器生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的(浅拷贝,或者值拷贝),而自定义类型是调用其拷贝构造函数完成拷贝的
为啥拷贝构造函数的参数只有一个且必须是引用传参
使用传值方式编译器直接报错,因为会引发无穷递归调用 !!
程序要调用拷贝构造函数就需要先传参,若传参使用传值传参,那么在传参过程中又需要进行对象的拷贝构造,如此循环往复,最终引发无穷递归调用。

程序要调用拷贝构造函数就需要先传参,若传参使用引用传参,那么在传参过程中无需进行对象的拷贝构造,便不会引发无穷递归调用

传值传参和引用传参
一、传值传参
传值传参是指将实际参数的值拷贝复制一份传递给函数的形式参数。在函数内部对形式参数的修改不会影响到实际参数的值。
void test(int a)
{
a++;
}
int main()
{
int sum = 10;
test(sum);
cout << "After test:" << sum << endl;
return 0;
}
在这个例子中,函数 test 接受一个整数参数 a ,在函数内部对 a 进行递增操作。但是,由于是传值传参,实际参数 sum 的值并没有被改变。

二、引用传参
引用传参是指将实际参数的引用(即地址)传递给函数的形式参数。在函数内部对形式参数的修改会直接影响到实际参数的值。
void test(int& a)
{
a++;
}
int main()
{
int sum = 10;
test(sum);
cout << "After test:" << sum << endl;
return 0;
}
在这个例子中,函数 test 接受一个整数引用参数 a,在函数内部对 a 进行递增操作。由于是引用传参,实际参数 sum 的值被改变了。

浅拷贝和深拷贝
一、浅拷贝(Shallow Copy)
- 浅拷贝是一种简单的对象复制方式,它只复制对象的成员变量的值
- 而对于指针成员变量,只是复制指针的值,而不是复制指针所指向的内容。这意味着多个对象可能共享相同的资源。
class Stack
{
public:
Stack(int capacity = 4)
{
_a = new int;
_size = 0;
_capacity = capacity;
}
void Print()
{
cout << _a << endl;// 打印栈空间地址
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack s1(10);
Stack s2 = s1;
s1.Print();
s2.Print();
return 0;
};
结果打印对象 s1 和 s2 的成员变量_a 的地址相同,这就意味着,就算在创建完s2后,我们对对象s1 做的任何操作都会直接影响到对象s2
在这个例子中,s1 和 s2 是两个 Stack 对象,它们通过浅拷贝进行复制。由于浅拷贝只是复制了指针的值,所以两个对象的 a 指针指向相同的内存地址,即它们共享了同一个栈空间

总结浅拷贝:
- 快速和简单:浅拷贝的实现通常比较简单,只需要复制对象的成员变量的值即可。因此,它的执行速度相对较快。
- 可能导致资源共享:由于浅拷贝只是复制指针的值,而不是复制指针所指向的内容,所以多个对象可能共享相同的资源。这可能会导致一些问题,例如当其中一个对象被销毁时,可能会影响其他对象的状态。
- 不适合复杂对象:对于包含动态分配内存或其他资源的复杂对象,浅拷贝可能不是一个合适的复制方式,因为它可能会导致资源泄漏或错误。
二、深拷贝(Deep Copy)
- 深拷贝是一种更彻底的对象复制方式,它不仅复制对象的成员变量的值
- 还复制指针所指向的内容。这意味着每个对象都有自己独立的资源,不会与其他对象共享。
class Stack
{
public:
Stack(int capacity = 4)
{
_a = new int[capacity];
_size = 0;
_capacity = capacity;
}
// 拷贝构造函数
Stack(const Stack& d)
{
_a = new int[d._capacity];
_size = 0;
_capacity = d._capacity;
}
void Print()
{
cout << _a << endl;// 打印栈空间地址
// cout << &_size << endl;
}
private:
int* _a;
int _size;
int _capacity;
};
int main()
{
Stack s1(10);
Stack s2 = s1;
s1.Print();
s2.Print();
return 0;
};
结果打印对象 s1 和 s2 的成员变量_a 的地址不相同,说明它们有各自独立的堆空间

在这个例子中,s1 和 s2 是两个 Stack 对象,它们通过深拷贝进行复制。由于深拷贝复制了指针所指向的内容,所以两个对象的 _a 指针指向不同的内存地址,即它们有各自独立的堆空间
深拷贝的总结:
- 资源独立性:深拷贝确保每个对象都有自己独立的资源,不会与其他对象共享。这可以避免资源共享带来的问题,如资源泄漏和错误。
- 复杂的实现:深拷贝的实现通常比较复杂,需要复制对象的所有成员变量,包括指针所指向的内容。对于包含大量资源或复杂数据结构的对象,深拷贝可能需要花费更多的时间和内存。
- 适合复杂对象:对于包含动态分配内存或其他资源的复杂对象,深拷贝是一个更合适的复制方式,因为它可以确保每个对象都有自己独立的资源,不会出现资源泄漏或错误。
赋值运算符重载
运算符重载
d1 = d2; // 可读性高(书写简单)
assignment(d1, d2); // 可读性差(书写麻烦)
Date& operator=(const Date& d)
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
- 运算符(.*)、运算符(::)、运算符(sizeof)、运算符(?:)运算符(.) 注意以上5个运算符不能重载
赋值运算符重载
重载运算符(=)
class Date
{
public:
// 构造函数
Date(int year = 2024, int month = 6, int day = 4)
{
_year = year;
_month = month;
_day = day;
}
// 拷贝构造函数
// d2(d1)
Date(const Date& d)
{
cout << "拷贝构造函数" << endl;
_year = d._year;
_month = d._month;
_day = d._day;
}
// 赋值运算符重载
// d2 = d3 ----> d2.operator=(&d2, d3)
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
// 析构函数
~Date()
{
cout << "~Date()" << endl;
}
void Print()
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
private:
int _year;
int _month;
int _day;
};
一、参数类型设置为引用,并用const进行修饰
赋值运算符重载函数的第一个形参默认是this指针,第二个形参是我们赋值运算符的右操作数。
- 第二个参数,由于是自定义类型传参,我们若是使用传值传参,会额外调用一次拷贝构造函数,所以函数的第二个参数最好使用引用传参。
- 第二个参数,我们在函数体内不会对其进行修改,所以最好加上const进行修饰。
二、函数的返回值使用引用返回
- 我们若是只以d2 = d1这种方式使用赋值运算符,赋值运算符重载函数就没必要有返回值,因为在函数体内已经通过this指针对d2进行了修改。
- 为了支持连续赋值,即d3 = d2 = d1,我们就需要为函数设置一个返回值了,而且很明显,返回值应该是赋值运算符的左操作数,即this指针指向的对象。
- 由于是自定义类型传参,我们若是使用传值返回,会额外调用一次拷贝构造函数,为了避免不必要的拷贝,我们最好还是使用引用返回,因为此时出了函数作用域this指针指向的对象并没有被销毁,所以可以使用引用返回
三、赋值前检查是否是给自己赋值
若是出现d1 = d1,我们不必进行赋值操作,因为自己赋值给自己是没有必要进行的。所以在进行赋值操作前可以先判断是否是给自己赋值,避免不必要的赋值操作。
四、引用返回的是*this
赋值操作进行完毕时,我们应该返回赋值运算符的左操作数,而在函数体内我们只能通过this指针访问到左操作数,所以要返回左操作数就只能返回*this。
五、一个类如果没有显示定义赋值运算符重载,编译器也会自动生成一个,完成对象按字节序的值拷贝
- 赋值运算符重载编译器也可以自动生成,并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的浅拷贝(值拷贝),例如d2 = d1,编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去。
- 对于日期类,编译器自动生成的赋值运算符重载函数就可以满足我们的需求,我们可以不用自己写。
- 但是这也不意味着所有的类都不用我们自己写赋值运算符重载函数,当遇到一些特殊的类,我们还是得自己动手写赋值运算符函数的。例如:包含动态分配资源的类、当类需要进行特殊的资源管理时(例如引用计数、互斥锁等:例如一个使用引用计数来管理资源的类,赋值操作可能需要更新引用计数,并且在引用计数为零时释放资源)
六、 赋值运算符只能重载成类的成员函数不能重载成全局函数
- 拷贝构造函数:用一个已经存在的对象去构造初始化另一个即将创建的对象。
- 赋值运算符重载函数:在两个对象都已经存在的情况下,将一个对象赋值给另一个对象。
const成员函数
我们将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰的是类成员函数隐含的this指针,表明在该成员函数中不能对this指针指向的对象进行修改。
我们可以对类成员函数中的打印函数进行const修饰,避免在函数体内不小心修改了对象
void Print()const// cosnt修饰的打印函数
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
思考下面几个问题(经典面试题):
- const对象可以调用非const成员函数吗?(不可以)
- 非const对象可以调用const成员函数吗?(可以)
- const成员函数内可以调用其他的非const成员函数吗?(不可以)
- 非cosnt成员函数内可以调用其他的cosnt成员函数吗?(可以)
解释:
- 非const成员函数,即成员函数的this指针没有被const所修饰,我们传入一个被const修饰的对象,用没有被const修饰的this指针进行接收,属于权限的放大,函数调用失败。
- const成员函数,即成员函数的this指针被const所修饰,我们传入一个没有被const修饰的对象,用被const修饰的this指针进行接收,属于权限的缩小,函数调用成功。
- 在一个被const所修饰的成员函数中调用其他没有被const所修饰的成员函数,也就是将一个被const修饰的this指针的值赋值给一个没有被const修饰的this指针,属于权限的放大,函数调用失败。
- 在一个没有被const所修饰的成员函数中调用其他被const所修饰的成员函数,也就是将一个没有被const修饰的this指针的值赋值给一个被const修饰的this指针,属于权限的缩小,函数调用成功。
注意:权限可以平移,不能放大,可以缩小
- 权限平移:const对象调用const成员函数
- 权限放大:const对象调用非const成员函数
- 权限缩小:非const对象调用const成员函数
取地址及const取地址操作符重载
取地址操作符重载和const取地址操作符重载,这两个默认成员函数一般不用自己重新定义,使用编译器自动生成的就行了。
class Date
{
public:
Date* operator&()// 取地址操作符重载
{
return this;
}
const Date* operator&()const// const取地址操作符重载
{
return this;
}
private:
int _year;
int _month;
int _day;
};
初始化列表
class Date
{
public:
// 构造函数
Date(int year = 2024, int month = 6, int day = 6)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
上列构造函数中的语句,只能被称为赋初始值,而不能称为初始化
初始化只有一次,而赋值可以很多次,就比如:
class Date
{
public:
// 构造函数
Date(int year = 2024, int month = 6, int day = 6)
{
_year = year;// 第一次赋值
_year = 2028;// 第二次赋值
//...
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
为什么要有初始化列表 ?
初始化列表有啥用啊,有赋值不就可以了吗?
前面我们说过,构造函数不是初始化是赋值,但是有的变量就必须初始化
- 引用的成员变量
- const成员变量
- 自定义类型成员
一、引用的成员变量
引用类型的变量在定义时就必须给其一个初始值,所以引用成员变量必须使用初始化列表对其进行初始化。
int a = 10;
int& b = a;// 创建时就初始化
二、const成员变量
被const修饰的变量也必须在定义时就给其一个初始值,也必须使用初始化列表进行初始化。
const int a = 10;//correct 创建时就初始化
三、自定义类型成员
若一个类没有默认构造函数,那么我们在实例化该类对象时就需要传参对其进行初始化,所以实例化没有默认构造函数的类对象时必须使用初始化列表对其进行初始化。
这是他们的特性导致的,所以这3个就必须使用初始化列表
如何初始化列表 ?
一、每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)
如下,初始化列表和构造函数的区别就是在{ }前开头用“ :”,接着是一个以逗号分隔的数据成员列表,每个"成员变量"后面跟一个放在括号中的初始值或表达式
class Date
{
public:
// 初始化列表
Date(int year = 2024, int month = 6, int day = 4)
:_year(year),_month(month),_day(day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
二、类中包含 引用的成员变量、const成员变量、自定义类型成员 ,必须放在初始化列表位置进行初始化
class A
{
public:
A(int a)
:_a(a)
{}
private:
int _a;
};
class B
{
public:
B(int a, int ref)
{}
private:
A _test; // 没有默认构造函数
int& _b1; // 引用的成员变量
const int _b2; // const成员变量
};
int main()
{
B b(10, 10);
return 0;
}
三、尽量使用初始化列表初始化
因为初始化列表实际上就是当你实例化一个对象时,该对象的成员变量定义的地方,所以无论你是否使用初始化列表,都会走这么一个过程(成员变量需要定义出来)。
1、对于内置类型,使用初始化列表和在构造函数体内进行初始化实际上是没有差别的,其差别就类似于如下代码:
// 使用初始化列表
int a = 10
// 在构造函数体内初始化(不使用初始化列表)
int a;
a = 10;
2、对于自定义类型,使用初始化列表可以提高代码的效率
四、成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关
class A
{
public:
A(int a)
:_a2(a++)
,_a1(a++)
{}
void Print()
{
cout << "_a1:" << _a1 << endl;
cout << "_a2:" << _a2 << endl;
}
private:
int _a1;
int _a2;
};
int main()
{
A a(1);
a.Print();
return 0;
}
explicit关键字
构造函数不仅可以构造和初始化对象,对于单个参数或者全缺省的构造函数,还支持隐式类型转换
class Date
{
public:
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 赋值运算符重载
Date& operator=(const Date &d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 6, 6);
d1.Print();
d1 = 2023;
d1.Print();
return 0;
}
在语法上,代码中d1 = 2023等价于以下两句代码:
Date tmp(2023); //先构造
Date d1(tmp); //再拷贝构造
在早期的编译器中,当编译器遇到 d1 = 2023 这句代码时,会先构造一个临时对象,再用临时对象拷贝构造d1;但是现在的编译器已经做了优化,当遇到 d1 = 2023 这句代码时,会按照Date d1(2023)这句代码处理,这就叫做隐式类型转换。
隐式类型转换中间会生成一个临时变量(临时变量具有常性)
尝试用非 const 引用绑定一个隐式类型转换的结果:
- 如果隐式类型转换过程中没有生成临时变量,那么非 const 引用应该可以直接绑定到隐式转换的源对象。
double i = 6.6;
int& j = i; // 编译错误
const int& j = i; // 编译成功
但实际上,用非 const 引用去绑定隐式类型转换的结果会导致编译错误。 这个编译错误暗示了在从 double 到 int 的隐式类型转换过程中,可能生成了一个临时变量,并且这个临时变量不能被非 const 引用绑定。(临时变量具有常性)

但是,对于单个参数或者全缺省的构造函数,d1 = 2023 这种代码的可读性不是很好,我们若是想禁止构造函数的隐式转换,可以用关键字explicit来修饰构造函数
class Date
{
public:
// 全缺省的构造函数
explicit Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 赋值运算符重载
Date& operator=(const Date &d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2024, 6, 6);
d1.Print();
d1 = 2023;
d1.Print();
return 0;
}
我们发现确实禁止了构造函数的隐式转换

static静态成员变量
有时候我们在设计程序的时候,我们可能需要一个变量他是属于整个类的,不属于某个对象。这个时候我们就需要一个静态成员变量。
- 声明为 static 的类成员称为类的静态成员。
- 用 static 修饰的成员变量,称之为静态成员变量;
- 用 static 修饰的成员函数,称之为静态成员函数。
- 静态成员变量一定要在类外进行初始化。
特性
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员,只能访问静态成员函数
- 静态成员也是类的成员,受访问限定符的限制
- 使用方法就是在变量前加一个static,然后再在类外定义
一、 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区
class A
{
public:
// 无参构造函数
A()
{
_a++;
}
// 拷贝构造函数
A(const A& d)
{
_a++;
}
~A()
{
_a--;
}
static int Get()
{
return _a;
}
private:
static int _a;
};
int A::_a = 0; // 静态成员变量一定要在类外进行初始化
int main()
{
cout << sizeof(A) << endl;
return 0;
}
类的大小为1,因为静态成员 _a 是存储在静态区的,属于整个类,也属于类的所有对象。所以计算类的大小或是类对象的大小时,静态成员并不计入其总大小之和。

静态成员函数可以调用非静态成员函数吗
不可以。因为非静态成员函数的第一个形参默认为this指针,而静态成员函数中没有this指针,故静态成员函数不可调用非静态成员函数。
非静态成员函数可以调用静态成员函数吗?
可以。因为静态成员函数和非静态成员函数都在类中,在类中不受访问限定符的限制。
友元
- 友元函数
- 友元类
友元函数
- 友元函数可访问类的私有和保护成员,但不是类的成员函数
- 友元函数不能用const修饰
- 友元函数可以在类定义的任何地方声明,不受类访问限定符限制
- 一个函数可以是多个类的友元函数
- 友元函数的调用与普通函数的调用原理相同
友元类
class A
{
// 声明B是A的友元类
friend class B;
public:
A(int n = 0)
:_n(n)
{}
private:
int _n;
};
class B
{
public:
void Test(A& a)
{
// B类可以直接访问A类中的私有成员变量
cout << a._n << endl;
}
};
- 友元关系是单向的,不具有交换性。例如上述代码中,B是A的友元,所以在B类中可以直接访问A类的私有成员变量,但是在A类中不能访问B类中的私有成员变量。
- 友元关系不能传递。如果A是B的友元,B是C的友元,不能得出A是C的友元。
内部类
如果一个类定义在另一个类的内部,则这个类被称为内部类。
注意:
- 此时的内部类是一个独立的类,它不属于外部类,更不能通过外部类的对象区调用内部类。
- 外部类对内部类没有任何优越的访问权限。
- 内部类就是外部类的友元类,即内部类可以通过外部类的对象参数来访问外部类中的所有成员。但是外部类不是内部类的友元。
特性
1、内部类可以定义在外部类的public、private以及protected这三个区域中的任一区域。
2、内部类可以直接访问外部类中的static、枚举成员,不需要外部类的对象/类名。
3、外部类的大小与内部类的大小无关。
#include <iostream>
using namespace std;
class A //外部类
{
public:
class B //内部类
{
private:
int _b;
};
private:
int _a;
};
int main()
{
cout << sizeof(A) << endl; //外部类的大小
return 0;
}
这里外部类A的大小为4,与内部类的大小无关
匿名函数
//有名对象,生命周期在当前局部域
A aa1(6);
//匿名对象,生命周期只在当前这一行,下一行就调用析构函数
A(8);
//但是有特例,比如
const A& ref = A(6);
//const引用会延长匿名对象的生命周期
//会到ref出作用域,匿名对象才会调用析构函数
3246





