类与对象:这个类也有自己的对象
1.面向过程和面向对象的区别
举一个简单的例子:
- 面向过程:关注处理问题的求解步骤
- 面向对象:关注处理问题的对象,将问题的求解步骤拆成对象之间的交互
2.何为类
- C语言的阶段,结构体只能定义变量;而C++,结构体可以定义变量和函数
typedef int DataType;
struct Stack
{
void Init(size_t capacity)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_capacity = capacity; _size = 0;
}
void Push(const DataType& data)
{
// 扩容
_array[_size] = data;
++_size;
}
DataType* _array;
size_t _capacity;
size_t _size;
};
- 但是C++中,我们常用class来替代结构体
2.1 类的定义
和结构体其实挺像的
class className
{
// 类体:由成员函数和成员变量组成
};
// 一定要注意后面的分号
- class为类的关键字,className为类的名字,{}包含类的属性、方法等(还有,注意分号)
- 类的属性叫成员变量,类的函数叫做成员函数
类的定义方式
①声明和定义全部放在类里面
class Data
{
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
int _year;
int _month;
int _day;
};
②声明放在.h,定义放在.cpp,,但是定义部分的成员函数名前要加类名::
- 平时用第一种多一些,但是工作中尽量使用第二种
- 要注意命名规则,成员变量要和成员函数的参数做出区别
class Date
{
public:
void Init(int year)
{
_year = year;
}
private:
int _year;
};
2.2 类的访问限定符及其封装
- 类本身是可以封装的:通过类将对象的属性与方法结合在一起,对象能更加完善,通过一定的访问权限也可以控制接口的访问
- 自然:访问权限也有对应的访问限定符来实现

注意:
①访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止

②如果后面没有访问限定符,作用域一直到类结束
③class的默认访问权限为private,struct为public
④访问限定符只在编译期间有用,当数据映射到内存后,就没有任何访问限定符上的区别了 - 不能依赖访问限定符来保护敏感数据(因为运行时可以被绕过)
- 访问限定符主要是用于帮助实现封装的
#include <iostream>
class Secret {
private:
int password = 12345; // private 成员
public:
void show() {
std::cout << "密码是:" << password << std::endl;
}
};
int main() {
Secret s;
// 正常方式:不能直接访问
// std::cout << s.password; // 编译错误
// 绕过方式:通过指针偏移直接访问内存
int* ptr = reinterpret_cast<int*>(&s);
std::cout << "通过内存直接访问:" << *ptr << std::endl; // 输出 12345
return 0;
}
面试题:struct和class的区别
①C++中的struct可以当成结构体使用,因为C++兼容C;C++中的struct也可以定义类,和class定义类是一样的
②但是struct的默认访问权限是public,class的默认访问权限是private
③继承上。默认继承权限不同
- struct是默认public继承,而class则是private继承;
- struct继承struct是public继承,class继承struct还是private继承
④模板参数的列表中的使用差异:
- class关键字可用于模板参数
- struct不能用于模板参数声明,标准C++中的模板参数只能用class或typename
2.3 封装
- 面向对象的三大特性:封装、继承、多态
- 封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互
- 计算机厂商在出厂相关设备时,通过外壳隐藏内部细节,仅仅向外提供开关机、鼠标以及键盘插孔等,让用户可以与计算机进行交互
- 通过类可以将数据以及操作数据的方法进行结合,通过访问权限隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用
2.4 类的作用域
- 类定义了一个新的作用域,类的所有成员都在类的作用域中
- 通过::这个作用域操作符,可以指明成员属于的域
class Person
{
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print();
int _year;
int _month;
int _day;
}
//要指明函数属于Person这个类域
void Person::Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
2.5 类的实例化
- 类的实例化:也就是用类的类型创建对象而已
- 类的实例化可以实例化多个对象,但是实例化后的对象本身不占用物理空间,只有进行了初始化等操作,实例化出来的对象才能够实际存储数据,占据物理空间

3.类对象模型
3.1 类对象的大小计算

- 本质上,其计算方式是和结构体相同的
- 但是一个类中还有成员函数,那一个实例化的类对象里面,该如何计算类的大小,就有说法了
3.2 类的存储方式
在下列三种方式中,我们可以猜测,具体的存储方式是哪种:
- ①对象中包含类的各个成员:类成员变量和类成员函数

- 但是每个对象中的成员变量不同,函数缺失相同的,创建多了就浪费空间了
②每个对象中,代码只保存一份只保存对象的属性和类成员函数的地址,毕竟类成员函数的代码都是相同的
③只保存成员变量,而成员函数直接放在公共的代码段
我们来具体实践一下:
- 可以发现,编译器就是按照方式③来存储的
- 空类的大小,默认为一个字节,负责唯一标识这个类的对象
3.3 结构体的内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
- 注意:对齐数 = 编译器默认的一个对齐数与该成员大小的较小值。 VS中默认的对齐数为8
- 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍
4.this指针
- 对于下面这种情况,我有时候会想:函数体中没有关于不同的对象的区分,那当d1调用Init函数的时候,函数如何知道它是d1对象呢?
class Date
{
public:
void Init(int year, int month, int day) {
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
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;
}
- C++中通过this指针,可以解决上面这个问题:C++编译器会给每个非静态的成员函数添加一个隐藏的指针参数,函数体中所有成员变量的操作,都通过指针去访问,用户不需要传递,而由编译器负责完成
d1.Print() <=> Print(&d1)
d2.Print() <=> Print(&d2)

this指针的特性
- this指针的类型:类类型*const,所以this指针是无法赋值的
- 只能在成员函数的内部使用
- this指针的本质是成员函数的形参
- this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx(x86)/rcx(x64)寄存器传递,而不用用户传递

面试题
①this指针存在哪里:
- this 指针本质上是成员函数的隐含形参,所以按传统函数参数的理解通常归到栈;
- 但从实际编译实现看,很多时候会先通过寄存器传递,不一定真的一直存放在栈中
②判断下面编译程序运行的结果
class A
{
public:
void Print()
{
cout << "Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
- 代码的核心是:A*p = nullptr; p->Print()
- 其中Print()立马只是打印一个Print()字符串,没有访问任何成员变量,也没有用到this执行的对象内容
- 所以很多编译环境下,它会被编译成一种“只是在调用普通函数逻辑”的效果,虽然写法是 p->Print(),但函数体里没解引用空指针对应的对象数据,因此通常可以正常运行并输出
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0;
}
- 代码的核心和上面一样
- 但是,这里PrintA中,对成员变量进行了访问
- 那就需要通过this指针,去取对象中的成员this->a
- 一个控制在取地址成员,运行时必然会出问题
C语言和C++实现Stack
typedef int DataType;
typedef struct Stack
{
DataType* array;
int capacity;
int size;
}Stack;
void StackInit(Stack* ps)
{
assert(ps);
ps->array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == ps->array)
{
assert(0);
return;
}
ps->capacity = 3;
ps->size = 0;
}
void StackDestroy(Stack* ps)
{
assert(ps);
if (ps->array)
{
free(ps->array);
ps->array = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
typedef int DataType;
class Stack
{
public:
void Init()
{
_array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = 3;
_size = 0;
}
void Push(DataType data)
{
CheckCapacity();
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
return;
_size--;
}
DataType Top(){ return _array[_size - 1];}
int Empty() { return 0 == _size;}
int Size(){ return _size;}
void Destroy()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
void CheckCapacity()
{
if (_size == _capacity)
{
int newcapacity = _capacity * 2;
DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
_array = temp;
_capacity = newcapacity;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
- C语言的实现中
- 每个函数的第一个参数都是Stack*
- 函数中必须要对第一个参数进行检测(因为可能会NULL)
- 函数中都通过Stack*来操作栈
- 调用时必须传递Stack结构体变量的地址
- 结构体只能定义存放数据的结构,操作数据的方法不能放在结构体中
- C++中,通过类可以将数据以及操作数据的方法完美结合,通过访问权限控制方法在类外可以被调用,即封装
5.类的6个默认成员函数
- 默认成员函数:用户自己没有实现,而是由编译器生成的成员函数,就是默认成员函数

- 现在,让我们一一介绍这些函数
5.1 构造函数
- 构造函数是一个特殊的成员函数,它负责让对象的每个数据成员都有一个合适的初值 - 初始化
- 在对象的生命周期中只调用一次
特性:
- ①函数名和类名相同
- ②无返回值(意思就是不需要写返回值)
- ③对象实例化的时候,就会自动调用对应的构造函数
- ④支持重载
#include <iostream>
using namespace std;
class Date
{
public:
//带参构造函数
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
//无参构造函数
Date()
{
_year = 0;
_month = 0;
_day = 0;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2025, 2, 20);
Date d2;
d1.Print();
d2.Print();
return 0;
}

- ⑤如果类中没有显示定义构造函数,C++编译器会默认生成一个无参的默认构造函数,一旦用户定义了,编译期间就将不再生成
#include <iostream>
using namespace std;
class Date
{
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
int _year;
int _month;
int _day;
};
int main()
{
Date d2;
d2.Print();
return 0;
}

目前C++11标准支持可以为自定义类型赋默认缺省值,这样的话即便没有显示定义构造函数,利用系统的也可以快捷操作了。
- ⑥C++把类型分成内置类型和自定义类型
- 内置类型是由语言提供的数据类型 - int、char等
- 自定义类型是使用class/struct/union等自己定义的类型
- 内置类型在变量中可以给默认声明值,但是自定义类型需要构造函数
- 编译器会对自定义类型成员构造其默认的成员函数
#include <iostream>
using namespace std;
class Time
{
public:
Time()
{
_hour = 0;
_minute = 0;
_second = 0;
}
int _hour;
int _minute;
int _second;
};
class Date
{
public:
void Print()
{
cout << _year << "/" << _month << "/" << _day << "/" << endl;
cout << _t._hour << "/" << _t._minute << "/" << _t._second << "/" << endl;
}
int _year=2026;
int _month=4;
int _day=3;
Time _t;
};
int main()
{
Date d2;
d2.Print();
return 0;
}

- ⑦无参的构造函数、全缺省的构造函数以及编译器默认生成的构造函数,都是默认构造函数
- 默认构造函数只能有一个 - 上述的三者只能选一个存在,不可以同时存在
- 平时最好就用全缺省的默认构造函数

初始化列表
- 初始化列表:以一个冒号开始,接着以一个逗号分隔的数据成员列表,每个成员变量后面都跟着一个放在括号中的初始值或者表达式
class Date
{
Date(int year,int month,int 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)
:_aobj(a)
,_ref(ref)
,_n(10)
{}
private:
A _aobj;
// 没有默认构造函数
int& _ref;
// 引用
const int _n; // const
};
- ③对于自定义类型成员变量,一定会先通过初始化列表中初始化
class Date
{
public:
Date(int day)
:_day(day)
a((int*)malloc(sizeof(int)*3))//可以用简化的函数初始化
{}
private:
int _day;
Time _t;
int* a;
}
- ④成员变量在类中的声明次序就是在初始化列表中的初始化顺序
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();//1 随机值
}
//先初始化a2,再初始化a1
注意事项:
- 即使内置类型没有在初始化列表显式初始化,但是也会走初始化列表初始化
- 构造函数的内部主要是做赋值,初始化都是交由初始化列表实现
explicit 关键字
- 一般而言,其实可以做下面的操作:
class Date
{
public:
Date(int year)
:_year(year)
{}
private:
int _year;
int _month;
int _day;
}
int main()
{
Date d3(2022);
d3 = 2023;
}
- 其中d3 = 2023,编译器会先构造一个对象(调用对应的构造函数),然后赋值给对象
- 但是如果加了explicit,就禁止构造函数的隐式转换,d3=2023这种操作就失效了


5.2 析构函数
- 析构函数不是去做销毁,而是完成销毁时对象资源的清理工作
特性
- ①析构函数需要在类名前加~
- ②没有参数,也没有返回值
- ③一个类只有一个析构函数,不可重载
- ④对象生命周期结束后,编译系统会自动调用析构函数
~Date()
{
_year = 0;
_month = 0;
_day = 0;
cout << "~Date" << endl;
}
- ⑤自定义类型的成员,会调用它自己的析构函数
- 内置类型会有系统将其内存进行回收
class Date
{
public:
Date()
{
_year = 0;
_month = 0;
_day = 0;
}
~Date()
{
_year = 0;
_month = 0;
_day = 0;
cout << "~Date" << endl;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << "/" << endl;
//cout << _t._hour << "/" << _t._minute << "/" << _t._second << "/" << endl;
}
int _year;
int _month;
int _day;
Time _t;
};
int main()
{
Date d2;
d2.Print();
return 0;
}

- ⑥如果一个类中存在申请资源,一定要自己写析构函数,否则会导致内存泄漏
class Stack
{
public:
Stack(size_t capacity = 3)
{
_array = (int*)malloc(sizeof(int) * capacity);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = capacity;
_size = 0;
}
void Push(int data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
bool Empty()
{
return _size == 0;
}
int Top()
{
return _array[_size - 1];
}
void Pop()
{
//..
}
~Stack()
{
cout << "~Stack()" << endl;
if (_array)
{
free(_array);
_array = nullptr;
}
_size = _capacity = 0;
}
private:
int* _array;
int _capacity;
int _size;
};
int main()
{
Stack d1;
Stack d2;
}

- ⑦不同对象析构的顺序:局部对象(先定义后析构)-> 局部的静态 -> 全局对象(后定义先析构)
- 静态变量和全局变量之间:谁后定义就谁先析构
class Date
{
public:
Date(int year = 1)
{
_year = year;
}
~Date()
{
cout << "~Date()->"<<_year<< endl;
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
};
void func()
{
Date d3(3);
static Date d4(4);
}
Date d5(5);
static Date d7(7);
Date d6(6);
static Date d8(8);
// 局部对象(后定义先析构) -》 局部的静态 -》全局对象(后定义先析构)
int main()
{
Date d1(1);
Date d2(2);
func();
return 0;
}

5.3 拷贝构造函数
- 创建一个双胞胎
- 它只有一个形参,这个形参是一个const引用
- 通过已创建的对象去创建新对象的时候,会自动调用
特征
- ①拷贝构造是构造函数的一个重载形式、
- ②拷贝构造函数的参数只有一个,且是类类型对象的引用
- 如果是传值,会引发无穷递归调用
class Date
{
public:
Date(int year = 2015,int month = 2,int day = 28)
{
_year = year;
_month = month;
_day = day;
}
~Date()
{
cout << "~Date()->"<<_year<< endl;
}
//用const可以避免被修改
//同时如果不用const的话,就无法实现const对象的拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2016,2,3);
//下面二者都会发生拷贝构造
Date d2(d1);
Date d3 = d2;
//const Date d4 = d3(如果参数为非const,就不可以,因为此时会产生权限的放大)
}

-
传值传参 - 传的是实参的拷贝,而不是实参本身,所以会发生拷贝构造,然后拷贝构造又调用拷贝构造,反反复复,无穷递归
-
③如果没有显示定义,编译器自己会生成默认的拷贝构造,按照字节序完成拷贝 - 浅拷贝/值拷贝
- 但是如果对象包含自定义类型,就会去调用它自己的拷贝构造函数
-
④编译器生成的默认拷贝构造只能完成字节序的值拷贝,但是如果存在一些malloc的资源(也就是在堆上申请的,动态开辟的资源),那就必须要自己实现拷贝构造
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
// Stack st2(st1);
Stack(const Stack& s)
{
DataType* tmp = (DataType*)malloc(s._capacity *(sizeof(DataType)));
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(tmp, s._array, sizeof(DataType) * s._size);
_array = tmp;
_size = s._size;
_capacity = s._capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
/*~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}*/
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
int main()
{
Stack st1;
Stack st2(st1);
return 0;
}

typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 10)
{
_array = (DataType*)malloc(capacity * sizeof(DataType));
if (nullptr == _array)
{
perror("malloc申请空间失败");
return;
}
_size = 0;
_capacity = capacity;
}
// Stack st2(st1);
Stack(const Stack& s)
{
DataType* tmp = (DataType*)malloc(s._capacity *(sizeof(DataType)));
if (tmp == nullptr)
{
perror("malloc fail");
exit(-1);
}
memcpy(tmp, s._array, sizeof(DataType) * s._size);
_array = tmp;
_size = s._size;
_capacity = s._capacity;
}
void Push(const DataType& data)
{
// CheckCapacity();
_array[_size] = data;
_size++;
}
~Stack()
{
if (_array)
{
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
private:
DataType* _array;
size_t _size;
size_t _capacity;
};
class MyQueue
{
private:
Stack _st1;
Stack _st2;
};
void func(const MyQueue& qq)
{}
int main()
{
Stack st1;
Stack st2(st1);
MyQueue q1;
MyQueue q2(q1);
func(q1);
return 0;
}
- ⑤拷贝构造函数典型调用场景
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
class Date
{
public:
Date(int year, int minute, int day)
{
cout<<"Date(int,int,int):"<<this<<endl;
}
Date(const Date& d)
{
cout<<"Date(const Date& d):"<<this<<endl;
}
~Date()
{
cout<<"~Date():"<<this<<endl;
}
private:
int _year;
int _month;
int _day;
}
Date Test(Date d)
{
Date temp(d);
return temp;
}
int main()
{
Date d1(2022,1,13);
Test(d1);//temp直接构成返回值对象,在这句语句之后才销毁
return 0;
}
- 析构顺序
- 普通局部对象
- 形参:函数结束时析构
- 返回的临时对象:如果没有接住,一般在整条表达式结束后析构


5.4 赋值运算符重载
运算符重载
- 运算符重载是一个具有特殊函数名的函数,它具有返回值类型,函数名字,参数列表,其实整体是和普通函数类似的
- 函数名:关键字operator后面需要接重载的运算符符号
- 函数原型:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,不能改变它本身的函数,例如:整形的+就是加法,不能改变其含义
- 作为类成员参数时,存在一个隐藏形参(this指针为第一个参数)
- 图片的5个运算符不不能够重载
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
int _year;
int _month;
int _day;
};
//bool operator==(const Date& d1, const Date& d2) {
// return d1._year == d2._year
// && d1._month == d2._month
// && d1._day == d2._day;
//}
int main()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
//d1.operator==(d2)
cout << (d1 == d2) << endl;
return 0;
}
赋值运算符重载
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this:要符合连续赋值的含义
//第一种:
void operator=(Date d);
/*
此时,无法解决连续赋值的问题
解释:
1.a = b = c <=> a = (b = c)
返回void时,赋值表达式没有结果值,所以,无法实现链式赋值
2. Date d - 这是传值调用,会有拷贝构造,而且会造成额外开销,不如const引用
*/
//第二种:
Date operator(const Date& d)
{
//...
return *this;
}
/*
解释:
1.返回类型为Date时,返回的时当前对象的一份拷贝,而不是对象本身的别名,也是可以链式赋值的
2.赋值运算符如果返回临时对象,虽然语法合法,但是会导致额外的拷贝构造
a = b = c 先执行b=3
此时返回一个Date临时对象
此时赋值给a时就会产生多一次拷贝构造
*/
//第三种:
Date& operator=(const Date& d)
{... return*this}
/*
1.参数用const Date& - 避免额外拷贝
2.返回Date&,支持链式赋值
3.返回的是当前对象本身,符合内置赋值运算符的行为
*/
- 赋值运算符只能重载成类的成员,而不能重载成全局函数
//没有this指针,就需要给两个参数
Date& operator=(Date& left,const Date& right)
{
if(&left != &right)
{
...
}
return left
}
- 因为如果赋值运算符不实现,编译器会给对象默认生成一个,如果我们自己在类外面再实现了一个,就可能会冲突
- 所以 - 赋值运算符重载只能是类的成员函数

- 用户没有显式实现,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
- 内置类型的成员变量直接赋值
- 自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值(和别的默认成员函数一样)
- 当涉及到资源申请时,必须要实现赋值运算符重载

- 下面是深拷贝的实现:
class Stack
{
public:
Stack(size_t capacity = 10)
: _size(0), _capacity(capacity)
{
_array = new int[_capacity];
}
Stack(const Stack& s)
: _size(s._size), _capacity(s._capacity)
{
_array = new int[_capacity];
for (size_t i = 0; i < _size; ++i)
{
_array[i] = s._array[i];
}
}
~Stack()
{
delete[] _array;
_array = nullptr;
_size = 0;
_capacity = 0;
}
Stack& operator=(const Stack& s)
{
if (this != &s)
{
int* tmp = new int[s._capacity];
for (size_t i = 0; i < s._size; ++i)
{
tmp[i] = s._array[i];
}
delete[] _array;
_array = tmp;
_size = s._size;
_capacity = s._capacity;
}
return *this;
}
private:
int* _array;
size_t _size;
size_t _capacity;
};
前置++和后置++重载
- 为了让前置++和后置++能够正确重载,这里添加一个int类型的参数,调用函数时,该参数不用传递,编译器自动传递即可
//前置++
Date& operator++()
{
_day += 1;
return *this;
}
//后置++
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
6.Date类的模拟实现
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month)
{
static int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30,
31};
int day = days[month];
if (month == 2 &&((year % 4 == 0 && year % 100 != 0) || (year%400 == 0)))
{
day += 1;
}
return day;
}
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
比特就业课
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置-
Date operator--(int);
// 前置-
Date& operator--();
// >运算符重载
bool operator>(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// >=运算符重载
bool operator >= (const Date& d);
// <运算符重载
bool operator < (const Date& d);
// <=运算符重载
bool operator <= (const Date& d);
// !=运算符重载
bool operator != (const Date& d);
// 日期-日期 返回天数
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
具体实现:
#include "Date.h"
Date::Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
bool Date::operator<(const Date& d)
{
if (_year < d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month < d._month)
return true;
else if (_month == d._month)
{
if (_day < d._day)
return true;
else
false;
}
else
return false;
}
return false;
}
bool Date::operator>(const Date& d)
{
if (_year > d._year)
{
return true;
}
else if (_year == d._year)
{
if (_month > d._month)
return true;
else if (_month == d._month)
{
if (_day > d._day)
return true;
else
false;
}
else
return false;
}
return false;
}
bool Date::operator<=(const Date& d)
{
return !(*this > d);
}
bool Date::operator>=(const Date& d)
{
return !(*this < d);
}
bool Date::operator==(const Date& d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
bool Date::operator!=(const Date& d)
{
return !(*this == d);
}
Date& Date::operator+=(int day)
{
_day += day;
while (_day > GetMonthDay(_year, _month))
{
_day -= GetMonthDay(_year, _month);
++_month;
if (_month > 12)
{
++_year;
_month = 1;
}
}
return *this;
}
Date Date::operator+(int day)
{
Date tmp(*this);
tmp += day;
return tmp;
}
Date Date::operator-(int day)
{
Date tmp(*this);
tmp -= day;
return tmp;
}
Date& Date::operator-=(int day)
{
_day -= day;
while (_day <= 0)
{
--_month;
if (_month < 1)
{
--_year;
_month = 12;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date:: operator++(int)
{
Date tmp(*this);
*this += 1;
return tmp;
}
Date Date::operator--(int)
{
Date tmp(*this);
*this -= 1;
return tmp;
}
Date& Date::operator--()
{
*this -= 1;
return *this;
}
// d1 - d2
int Date::operator-(const Date& d)
{
int flag = 1;
Date max = *this;
Date min = d;
if (*this < d)
{
flag = -1;
max = d;
min = *this;
}
int n = 0;
while (min != max)
{
++min;
++n;
}
return n * flag;
}
7.static成员
- 静态成员为所有类共享,不仅仅属于某个具体的对象,存放在静态区
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类中的静态成员可以用类名::静态成员或对象.静态成员来访问
- 要用上述两种方式访问,都需要静态变量本身的权限在public才行
- 不然要访问静态成员变量,只能通过成员函数或者友元函数
- 静态成员函数没有隐藏的this指针,无法访问任何静态成员
- 静态成员也是类的成员,受访问限定符的限制

注意:
- 静态成员函数可以调用非静态成员函数嘛?
- 不能直接调用 - 没有this指针
#include <iostream>
using namespace std;
class A
{
public:
int _x = 10;
void Print()
{
cout << "_x = " << _x << endl;
}
static void Test()
{
// Print(); // 错误
// cout << _x; // 错误
}
};
int main()
{
A a;
a.Print();
A::Test();
return 0;
}
- 可以间接调用 - 通过对象调用
#include <iostream>
using namespace std;
class A
{
public:
int _x = 10;
void Print()
{
cout << "_x = " << _x << endl;
}
static void Test()
{
A a; // 静态函数里自己造一个对象
a.Print(); // 通过对象调用非静态成员函数
}
};
int main()
{
A::Test();
return 0;
}
- 非静态成员函数可以调用静态成员函数嘛
- 可以,静态成员属于整个类,而不是具体的对象
8.友元
8.1 友元函数
- 假如现在要重载operator<<,可以发现是不行的,因为输出流对象和隐含的this指针都在抢占第一个参数的位置
- this指针默认是第一个参数,而cout需要是第一个形参才能正常使用
- 所以,要将operator重载成一个全局函数
- 但是此时,类外没办法访问成员,此时就可以通过友元解决
class Date
{
friend ostream& operator<<(ostream& _cout, const Date& d);
friend istream& operator>>(istream& _cin, Date& d);
public:
Date(int year, int month, int day)
:
_year(year),
_month(month),
_day(day)
{}
// d1 << cout; -> d1.operator<<(&d1, cout); 不符合常规调用
// 因为成员函数第一个参数一定是隐藏的this,所以d1必须放在<<的左侧
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& _cout,const Date&d)
{
_cout <<d._year << "-" << d._month << "-" << d._day << endl;
return _cout;
}
istream& operator>>(istream& _cin, Date& d)
{
_cin >> d._year;
_cin >> d._month;
_cin >> d._day;
return _cin;
}
int main()
{
Date d1(2013, 2, 3);
cout << d1 << endl;
}
特性
- ①友元函数可以访问类的私有和保护成员,但是不是类的成员函数
- ②友元函数不能用const修饰
- ③友元函数可以在类的任何地方声明,不受类访问限定符的限制
#include <iostream>
using namespace std;
class A
{
public:
A(int x = 0)
: _x(x)
{}
private:
int _x;
public:
friend void f1(const A& a); // 放在 public 里
private:
friend void f2(const A& a); // 放在 private 里
protected:
friend void f3(const A& a); // 放在 protected 里
};
void f1(const A& a)
{
cout << "f1: " << a._x << endl;
}
void f2(const A& a)
{
cout << "f2: " << a._x << endl;
}
void f3(const A& a)
{
cout << "f3: " << a._x << endl;
}
int main()
{
A a(100);
f1(a);
f2(a);
f3(a);
return 0;
}
- ④一个函数可以是多个类的友元函数
- ⑤友元函数的调用和普通函数的调用原理相同
8.2 友元类
- 友元类的所有成员函数都可以是另一个类的友元函数,都可以访问另一个类中的非公有成员
- 友元关系是单向的,不具有交换性:Time类和Date类,即便Time类中声明Date为友元类,Date可以访问Time中的私有成员变量,但是Time想访问Date却不可以
- 友元关系无法传递:C为B的友元,B为A的友元,不代表C是A的友元
- 友元关系不能继承
9.内部类
- 一个类的的定义在另一个类的内部,这个类就是友元类
- 其次:内部类就是外部类的友元类
- 内访问外:可以;外访问内:不行
- 内部类可以定义在外部类的任何访问限定符区域中
- 内部类可以直接访问外部类的static成员
- sizeof(外部类) = 外部类,和内部类没有任何关系
- 只有真的创建了对应的内部类对象,让它成为了外部类的成员对象,才会计入sizeof(内部类)
#include <iostream>
using namespace std;
class Outer
{
public:
class Inner
{
public:
int b;
};
private:
int a;
Inner obj; // 这才是“成员对象”
};
int main()
{
cout << "sizeof(Outer) = " << sizeof(Outer) << endl;
cout << "sizeof(Outer::Inner) = " << sizeof(Outer::Inner) << endl;
return 0;
}
class A
{
private:
static int k;
int h;
public:
class B // B天生就是A的友元
{
public:
void foo(const A& a)
{
cout << k << endl;//OK
cout << a.h << endl;//OK
}
};
};
int A::k = 1;
int main()
{
A::B b; //B b - ❌
//如果B在A的private下,甚至A::B b都不能用
b.foo(A());
}
10. 匿名对象
- 不用取名字,声明周期只有一行
- 一个没有名字的临时对象
class A
{
public:
A()
{
cout << "A()" << endl;
}
~A()
{
cout << "~A()" << endl;
}
};
int main()
{
A(); // 匿名对象
}
- 创建出来后会调用构造函数,但是这行结束后,就立马调用析构销毁了
- 它有以下几个利用场景:
- 直接当函数参数传进去
- 调用成员函数
- 作为返回值形成临时对象:
- 通过const引用可以延长匿名对象的寿命
const A& ra = A();
A() 创建了匿名对象
ra 绑定到这个匿名对象
这个匿名对象的生命周期会延长到 ra 的作用域结束
#include <iostream>
using namespace std;
class A
{
public:
A() { cout << "构造" << endl; }
~A() { cout << "析构" << endl; }
};
int main()
{
const A& ra = A();
cout << "中间" << endl;
return 0;
}
构造
中间
析构
- 普通引用不能绑定匿名对象,因为A()是临时对象,具有常属性,只能用const引用绑定

11.拷贝对象时的一些编译器优化
class A
{
public:
A(int a = 0)
:_a(a)
{
cout << "A(int a)" << endl;
}
A(const A& aa)
:_a(aa._a)
{
cout << "A(const A& aa)" << endl;
}
A& operator=(const A& aa)
{
cout << "A& operator=(const A& aa)" << endl;
if (this != &aa)
{
_a = aa._a;
}
return *this;
}
~A()
{
cout << "~A()" << endl;
}
private:
int _a;
};
void f1(A aa)
{}
A f2()
{
A aa;
return aa;
}
int main()
{
// 传值传参
A aa1;
f1(aa1);
cout << endl;
// 传值返回
f2();
cout << endl;
// 隐式类型,连续构造+拷贝构造->优化为直接构造
f1(1);
// 一个表达式中,连续构造+拷贝构造->优化为一个构造
f1(A(2));
cout << endl;
// 一个表达式中,连续拷贝构造+拷贝构造->优化一个拷贝构造
A aa2 = f2();
cout << endl;
// 一个表达式中,连续拷贝构造+赋值重载->无法优化
aa1 = f2();
cout << endl;
return 0;
}
12. const成员
- const修饰的成员函数,就是const成员函数
- const修饰成员函数,本质上就是修饰对应的this指针

- const成员不可以调用非const成员函数
- 因为 const 对象会把 this 视为 const 类名*,而非 const 成员函数默认需要的是可修改对象的 this
- 非const对象可以调用const成员函数
- 因为const成员函数承诺“不修改对象”,普通对象自然也能够调用
- const成员函数一般不可以调用其它的非const成员函数
- 在const成员函数里,this也是const 类名*,所以不能再通过当前对象去调用可能修改对象状态的非const成员函数
- 但是如果调用的是别的非const对象的非const函数,那倒可以
- 非const成员函数内可以调用其它的const成员函数
- 因为非 const 对象既能调用非 const 函数,也能调用 const 函数
#include <iostream>
using namespace std;
class A {
public:
void f1() { // 非const成员函数
cout << "f1 非const\n";
}
void f2() const { // const成员函数
cout << "f2 const\n";
// f1(); // 错误:const成员函数中不能直接调用当前对象的非const成员函数
}
void f3() { // 非const成员函数
f2(); // 对:非const函数可以调用const函数
}
};
int main() {
A a;
const A b;
a.f1(); // 对
a.f2(); // 对
b.f2(); // 对
// b.f1(); // 错:const对象不能调用非const成员函数
}
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)