文章目录
- 类和对象一
-
- [1. 类的定义](#1. 类的定义)
-
- [1.1 类定义格式](#1.1 类定义格式)
-
- 示例1:Stack类基础定义
- 示例2:Date类(成员变量带下划线区分)
- [示例3:C++ struct 升级对比](#示例3:C++ struct 升级对比)
- [1.2 访问限定符](#1.2 访问限定符)
- [1.3 类域](#1.3 类域)
-
- [示例:声明定义分离 + 类域::](#示例:声明定义分离 + 类域::)
- [2. 实例化](#2. 实例化)
-
- [2.1 实例化概念](#2.1 实例化概念)
- [2.2 对象大小](#2.2 对象大小)
- [3. this指针](#3. this指针)
- [4. C vs C++ Stack 对比(封装演示)](#4. C vs C++ Stack 对比(封装演示))
- 类和对象二
-
- [1. 类的6个默认成员函数](#1. 类的6个默认成员函数)
-
- [1.1 什么是默认成员函数](#1.1 什么是默认成员函数)
- [1.2 学习两个核心问题](#1.2 学习两个核心问题)
- [1.3 两种数据类型区分](#1.3 两种数据类型区分)
- [2. 构造函数(对象出生时自动初始化)](#2. 构造函数(对象出生时自动初始化))
-
- [2.1 作用](#2.1 作用)
- [2.2 7条特点(大白话)](#2.2 7条特点(大白话))
- [2.3 简单示例](#2.3 简单示例)
- [2.4 易错点](#2.4 易错点)
- [3. 析构函数(对象死亡时清理资源)](#3. 析构函数(对象死亡时清理资源))
-
- [3.1 作用](#3.1 作用)
- [3.2 8条特点](#3.2 8条特点)
- [3.3 简单示例](#3.3 简单示例)
- [4. 拷贝构造函数(用老对象造新对象)](#4. 拷贝构造函数(用老对象造新对象))
-
- [4.1 作用](#4.1 作用)
- [4.2 6条特点](#4.2 6条特点)
- [4.3 基础示例](#4.3 基础示例)
- [5. 赋值运算符重载(两个现成对象互相复制)](#5. 赋值运算符重载(两个现成对象互相复制))
-
- [5.1 运算符重载基础](#5.1 运算符重载基础)
-
- [5.1.1 概念](#5.1.1 概念)
- [5.1.2 基础规则](#5.1.2 基础规则)
- [5.2 赋值重载 operator=](#5.2 赋值重载 operator=)
-
- [5.2.1 和拷贝构造核心区别](#5.2.1 和拷贝构造核心区别)
- [5.2.2 特点](#5.2.2 特点)
- [5.2.3 简单示例](#5.2.3 简单示例)
- [6. const成员函数 + 取地址重载](#6. const成员函数 + 取地址重载)
-
- [6.1 const成员函数](#6.1 const成员函数)
-
- [6.1.1 写法](#6.1.1 写法)
- [6.1.2 作用](#6.1.2 作用)
- [6.1.3 调用规则](#6.1.3 调用规则)
- [6.1.4 示例](#6.1.4 示例)
- [6.2 取地址运算符重载](#6.2 取地址运算符重载)
-
- [6.2.1 两种重载](#6.2.1 两种重载)
- [6.2.2 使用场景](#6.2.2 使用场景)
- [6.2.3 极简示例](#6.2.3 极简示例)
- [7. const写在函数前、后,以及前后都写的区别](#7. const写在函数前、后,以及前后都写的区别)
-
- [7.1 const 在函数后方(成员函数专属)](#7.1 const 在函数后方(成员函数专属))
- [7.2 const 在函数前方(修饰返回值)](#7.2 const 在函数前方(修饰返回值))
- [7.3 const 前后都写](#7.3 const 前后都写)
- 类和对象三
-
- [1. 构造函数初始化列表](#1. 构造函数初始化列表)
-
- [1.1 基础语法](#1.1 基础语法)
- [1.2 三大**必须**放初始化列表的成员(函数体内赋值报错)](#1.2 三大必须放初始化列表的成员(函数体内赋值报错))
- [1.3 核心规则](#1.3 核心规则)
- [1.4 易错示例](#1.4 易错示例)
- [2. 隐式类型转换 & explicit关键字](#2. 隐式类型转换 & explicit关键字)
-
- [2.1 隐式转换规则](#2.1 隐式转换规则)
- [2.2 explicit作用](#2.2 explicit作用)
- [3. static 静态成员](#3. static 静态成员)
-
- [3.1 静态成员变量](#3.1 静态成员变量)
- [3.2 静态成员函数](#3.2 静态成员函数)
- [3.3 访问权限](#3.3 访问权限)
- [3.4 经典示例:统计对象个数](#3.4 经典示例:统计对象个数)
- [3.5 经典考题:1+2+...+n](#3.5 经典考题:1+2+…+n)
- [4. 友元 friend](#4. 友元 friend)
-
- [4.1 友元函数](#4.1 友元函数)
- [4.2 友元类](#4.2 友元类)
- [4.3 两大特性](#4.3 两大特性)
- [4.4 优缺点](#4.4 优缺点)
- [5 内部类(嵌套类)](#5 内部类(嵌套类))
-
- [5.1 定义](#5.1 定义)
- [5.2 核心特点](#5.2 核心特点)
- [5.3 使用场景](#5.3 使用场景)
- [6 匿名对象](#6 匿名对象)
-
- [6.1 定义](#6.1 定义)
- [6.2 区分易错](#6.2 区分易错)
- [7 对象拷贝的编译器优化](#7 对象拷贝的编译器优化)
-
- [7.1 优化目的](#7.1 优化目的)
- [7.2 典型优化场景](#7.2 典型优化场景)
- [7.3 不优化场景](#7.3 不优化场景)
- [7.4 关闭优化](#7.4 关闭优化)
- [7.5 优化差异](#7.5 优化差异)

类和对象一
1. 类的定义
1.1 类定义格式
class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:
- 类中的变量:类的属性 / 成员变量
- 类中的函数:类的方法 / 成员函数
规范习惯:为区分成员变量,常在变量前后加
_、或用m开头(C++非强制,按公司编码规范)。
- C++中
struct也可以定义类- C++兼容C语言
struct用法,同时struct升级为类,支持内部定义函数; - 日常开发优先用
class定义类; - 定义在类内的成员函数默认是
inline内联函数。
- C++兼容C语言
示例1:Stack类基础定义
cpp
#include<iostream>
using namespace std;
class Stack
{
public:
// 成员函数
void Init(int n = 4)
{
array = (int*)malloc(sizeof(int) * n);
if (nullptr == array)
{
perror("malloc申请空间失败");
return;
}
capacity = n;
top = 0;
}
void Push(int x)
{
// ...扩容逻辑
array[top++] = x;
}
int Top()
{
assert(top > 0);
return array[top - 1];
}
void Destroy()
{
free(array);
array = nullptr;
top = capacity = 0;
}
private:
// 成员变量
int* array;
size_t capacity;
size_t top;
}; // 分号不能省略
int main()
{
Stack st;
st.Init();
st.Push(1);
st.Push(2);
cout << st.Top() << endl;
st.Destroy();
return 0;
}
示例2:Date类(成员变量带下划线区分)
cpp
class Date
{
public:
void Init(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
// 习惯:成员变量加下划线区分局部变量
int _year; // year_ / m_year 也可
int _month;
int _day;
};
int main()
{
Date d;
d.Init(2024, 3, 31);
return 0;
}
示例3:C++ struct 升级对比
cpp
#include<iostream>
using namespace std;
// C语言风格struct(需要typedef)
typedef struct ListNodeC
{
struct ListNodeC* next;
int val;
}LTNode;
// C++ struct:无需typedef,自带类型名,可包含成员函数
struct ListNodeCPP
{
void Init(int x)
{
next = nullptr;
val = x;
}
ListNodeCPP* next;
int val;
};
int main()
{
return 0;
}
1.2 访问限定符
访问限定符是C++实现封装的核心方式:把属性、方法封装在类内,通过权限控制对外暴露接口。
public:公有成员,类外可直接访问private:私有成员,类外不可直接访问(默认权限)protected:保护成员,类外不可直接访问;仅在继承时和private有区别
规则:
- 权限作用域:从当前限定符开始,到下一个限定符为止;无后续限定符则到类末尾
}; class默认权限:private;struct默认权限:public;- 工程规范:成员变量设为
private/protected,对外提供调用的成员函数设为public。
1.3 类域
类定义了独立的作用域,所有类成员都属于该类域。
- 若成员函数声明与定义分离 ,类外实现时必须用
类名::作用域操作符指明所属类; - 编译器查找规则:先在局部域查找,找不到则去类域查找成员变量。
示例:声明定义分离 + 类域::
cpp
#include<iostream>
using namespace std;
class Stack
{
public:
// 函数声明
void Init(int n = 4);
private:
int* array;
size_t capacity;
size_t top;
};
// 类外定义函数,必须加 Stack:: 指定类域
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;
}
2. 实例化
2.1 实例化概念
- 实例化:使用类类型在内存中创建对象的过程;
- 类是抽象模板,仅声明成员变量,不分配内存;实例化对象时才会分配物理内存存储数据;
- 一个类可以实例化多个独立对象,每个对象拥有独立成员变量空间;
- 通俗比喻:类 = 建筑图纸,对象 = 按图纸造出的实体房子;图纸不能住,房子才能存数据。
示例:Date实例化多个对象
cpp
#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()
{
// 实例化两个独立对象d1、d2
Date d1;
Date d2;
d1.Init(2024, 3, 31);
d1.Print();
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
2.2 对象大小
存储核心结论
- 对象仅存储成员变量,不存储成员函数;
- 成员函数编译为指令,统一存放在公共代码段,所有对象共享同一份函数代码;
- 若每个对象都存函数指针会造成大量内存浪费;普通场景编译期直接绑定函数地址,无需存储指针;多态虚函数场景才会保存虚表指针。
两种存储设计对比:
- 设计一(废弃):对象内部同时存成员变量+所有成员函数,极度浪费空间;
- 设计二(C++标准):每个对象独占成员变量;成员函数全局共享公共代码区。
内存对齐规则(类对象遵循和结构体相同规则)
使用原因:CPU 读取内存不是一字节一字节读,而是按固定块(4/8 字节)批量读取。
- 若数据地址不对齐,CPU 需要读两次内存再拼接,效率极低;
- 部分硬件架构(ARM)不支持非对齐访问,直接崩溃;
- 编译器自动填充空白字节(填充 padding),保证每个成员地址对齐
对齐数 = min (编译器默认对齐数,当前成员变量自身大小)
- 第一个成员变量偏移地址为0;
- 其他成员对齐到「对齐数」整数倍地址:对齐数 =
编译器默认对齐数和变量自身大小的较小值;- VS默认对齐数:8;
- 类/结构体总大小 = 最大对齐数的整数倍;
- 嵌套结构体:嵌套子结构体对齐到自身最大对齐数倍数,整体大小取全部最大对齐数公倍数。
空类/无成员变量类大小特殊规则
cpp
#include<iostream>
using namespace std;
class A
{
private:
char _ch;
int _i;
};
class B
{
public:
void Print(){}
};
class C
{};
int main()
{
A a;
B b;
C c;
cout << sizeof(a) << endl; // 内存对齐计算
cout << sizeof(b) << endl; // 1字节
cout << sizeof(c) << endl; // 1字节
return 0;
}
空类、只有成员函数无成员变量的类,实例化对象占1字节:仅用于占位,标识该对象真实存在,区分不同空对象地址。
3. this指针
核心问题
同一个类多个对象调用同名成员函数,函数如何区分当前操作的是哪个对象?
this指针原理
-
编译器编译成员函数时,会自动在形参第一个位置 添加当前类指针参数
Date* const this;
例如void Init(int year,int month,int day)真实原型:cppvoid Init(Date* const this, int year, int month, int day) -
对象调用成员函数时,编译器自动把对象地址传给
this形参; -
函数内访问成员变量本质是
this->_year,this->可省略; -
语法限制:不能手动在形参/实参位置写
this;函数体内可显式使用this; -
this是Date* const类型:指针本身不可修改(不能this = nullptr),指向的内容可修改。
this指针完整示例
cpp
#include<iostream>
using namespace std;
class Date
{
public:
// 隐藏参数 Date* const this
void Init(int year, int month, int day)
{
// this = nullptr; // 编译报错,this指针不可修改指向
_year = year; // 等价 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;
// 编译器底层:d1.Init(&d1, 2024, 3, 31);
d1.Init(2024, 3, 31);
d1.Print();
d2.Init(2024, 7, 5);
d2.Print();
return 0;
}
配套选择题
第1题
cpp
#include<iostream>
using namespace std;
class A
{
public:
void Print()
{
cout << "A::Print()" << endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->Print();
return 0;
}
选项:A、编译报错 B、运行崩溃 C、正常运行
答案选C.
解析:首先,我们知道成员函数不存放在对象里,全部存放在公共代码段,当p->Print()调用函数时,不会去p的对象里寻找,而是直接锁定 Print 函数固定代码地址,直接调用。而Print()函数内部也没有调用对象里的内容,所以p并没有被解引用,也就不会访问空指针,程序就不会崩溃.
选学拓展:编译器如何在公共代码段找到 Print 函数
类的普通成员函数统一存放在程序公共代码段,编译、链接阶段编译器就会完成函数地址绑定,运行时完全不需要依靠对象指针查找函数。编译器识别p->Print()时,只会根据指针p的类型A*匹配对应类的A::Print,并从符号表中读取该函数在代码段的固定内存地址,将代码直接转换为固定跳转指令,仅把p的值作为隐藏形参this传入函数;整个查找过程与p是否为空、p指向的对象内存无关。只有带virtual修饰的虚函数才需要运行时通过对象内部虚表指针查找函数地址,本题 Print 为普通成员函数,不存在该步骤,因此哪怕p = nullptr,程序也能正常定位并进入 Print 函数执行,再加上函数内部没有访问对象成员、不会对空指针this解引用,最终程序正常运行。
第2题
cpp
#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;
}
选项:A、编译报错 B、运行崩溃 C、正常运行
答案:B;
解析:这里的_a实际为this->_a,空指针遭到解引用,程序崩溃;
4. C vs C++ Stack 对比(封装演示)
面向对象三大特性:封装、继承、多态,Stack对比直观体现封装思想。
核心变化
- C语言:数据(结构体)、操作函数完全分离,可随意修改结构体成员;
- C++:数据+成员函数封装进类,通过
private限制外部直接访问,仅暴露public接口,规范数据读写; - C++语法便利:缺省参数、无需手动传对象地址(this指针隐式传递)、不用typedef简化类型使用。
C语言实现栈
cpp
#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>
typedef int STDataType;
typedef struct Stack
{
STDataType* a;
int top;
int capacity;
}ST;
void STInit(ST* ps)
{
assert(ps);
ps->a = NULL;
ps->top = 0;
ps->capacity = 0;
}
void STDestroy(ST* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->top = ps->capacity = 0;
}
void STPush(ST* ps, STDataType x)
{
assert(ps);
if (ps->top == ps->capacity)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;
ps->capacity = newcapacity;
}
ps->a[ps->top] = x;
ps->top++;
}
bool STEmpty(ST* ps)
{
assert(ps);
return ps->top == 0;
}
void STPop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
ps->top--;
}
STDataType STTop(ST* ps)
{
assert(ps);
assert(!STEmpty(ps));
return ps->a[ps->top - 1];
}
int STSize(ST* ps)
{
assert(ps);
return ps->top;
}
int main()
{
ST s;
STInit(&s);
STPush(&s, 1);
STPush(&s, 2);
STPush(&s, 3);
STPush(&s, 4);
while (!STEmpty(&s))
{
printf("%d\n", STTop(&s));
STPop(&s);
}
STDestroy(&s);
return 0;
}
C++实现栈
cpp
#include<iostream>
using namespace std;
typedef int STDataType;
class Stack
{
public:
void Init(int n = 4)
{
_a = (STDataType*)malloc(sizeof(STDataType) * n);
if (nullptr == _a)
{
perror("malloc申请空间失败");
return;
}
_capacity = n;
_top = 0;
}
void Push(STDataType x)
{
if (_top == _capacity)
{
int newcapacity = _capacity * 2;
STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
if (tmp == NULL)
{
perror("realloc fail");
return;
}
_a = tmp;
_capacity = newcapacity;
}
_a[_top++] = x;
}
void Pop()
{
assert(_top > 0);
--_top;
}
bool Empty()
{
return _top == 0;
}
int Top()
{
assert(_top > 0);
return _a[_top - 1];
}
void Destroy()
{
free(_a);
_a = nullptr;
_top = _capacity = 0;
}
private:
// 私有成员变量,外部无法直接访问
STDataType* _a;
size_t _capacity;
size_t _top;
};
int main()
{
Stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
while (!s.Empty())
{
printf("%d\n", s.Top());
s.Pop();
}
s.Destroy();
return 0;
}
类和对象二
1. 类的6个默认成员函数
1.1 什么是默认成员函数
你写一个类,不写任何函数时,编译器偷偷自动生成的函数,就叫默认成员函数。
一共6个:构造、析构、拷贝构造、赋值重载、普通取地址、const取地址。
1.2 学习两个核心问题
- 编译器自动生成的函数能干啥?够不够用?
- 不够用时,自己该怎么写?
1.3 两种数据类型区分
- 内置类型:int、char、double、指针(C++自带基础类型)
- 自定义类型:自己用class写的类(Date、Stack)
2. 构造函数(对象出生时自动初始化)
2.1 作用
对象刚创建时,自动给成员变量赋初始值,不用手动调用Init初始化函数。
注意:不是构造函数开辟内存,局部对象的内存本来就开好,构造只负责赋值。
2.2 7条特点(大白话)
- 函数名字必须和类名一模一样
- 没有返回值,不能写void
- 创建对象时,系统自动调用,不用手动调用
- 可以写多个构造(函数重载)
- 只要你手动写了任意一个构造,编译器就不再自动生成无参构造
- 默认构造:不用传参数就能调用的构造,分三种:
- 编译器自动生成的无参构造
- 自己写的无参构造 Date()
- 全缺省构造 Date(int y=1,int m=1,int d=1)
无参和全缺省不能同时写,创建对象会分不清调用谁,报错。
- 自动构造初始化规则:
- 内置类型:值随机,不会清零
- 自定义类型成员:自动调用它自己的无参构造;如果这个类没有无参构造,代码报错
2.3 简单示例
cpp
#include<iostream>
using namespace std;
class Date
{
public:
// 无参构造(默认构造)
Date()
{
_y = 2000;
}
// 带参构造
Date(int y)
{
_y = y;
}
void Show()
{
cout << _y << endl;
}
private:
int _y;
};
int main()
{
Date d1; // 调用无参构造
Date d2(2025);// 调用带参构造
d1.Show();
d2.Show();
return 0;
}
2.4 易错点
写无参对象不能加括号 Date d3();,编译器会当成函数声明,不是创建对象。
3. 析构函数(对象死亡时清理资源)
3.1 作用
对象销毁前自动调用,释放堆上手动开辟的内存(malloc/new出来的空间)。
对象本身占用的栈内存系统自动回收,析构只管堆资源清理,替代C语言Destroy函数。
3.2 8条特点
- 名字:~加类名 ~Date()
- 无参数、无返回值
- 一个类只能有1个析构,不能重载
- 对象生命周期结束,系统自动调用
- 默认析构行为:
- 内置类型:啥也不干
- 自定义类型成员:自动调用它的析构
- 不管写不写析构,里面的自定义成员都会自动调用析构
- 要不要手动写析构:
- 没有堆内存(Date只有int):不用写,编译器默认够用
- 有堆内存(Stack里存指针数组):必须手写,否则内存泄漏
- 局部对象销毁顺序:先创建的后销毁,后创建的先销毁
3.3 简单示例
cpp
#include<iostream>
#include<cstdlib>
using namespace std;
class Stack
{
public:
Stack()
{
arr = (int*)malloc(sizeof(int)*10);
}
// 析构释放堆内存
~Stack()
{
cout << "释放堆空间" << endl;
free(arr);
arr = nullptr;
}
private:
int* arr;
};
int main()
{
Stack st;
// 函数结束,st销毁,自动执行析构
return 0;
}
4. 拷贝构造函数(用老对象造新对象)
4.1 作用
拿一个已经存在的同类对象,复制一份生成全新对象。
标准写法:Date(const Date& d)
4.2 6条特点
- 属于构造函数的重载
- 第一个参数必须是同类引用,不能直接传对象(会无限递归,直接报错)
- 三种场景会自动调用拷贝构造:
- 对象作为函数参数,值传递
- 函数返回对象,值返回
Date d2(d1); Date d2 = d1;用旧对象创建新对象
- 不手写,编译器自动生成默认拷贝构造:
- 内置变量:浅拷贝(直接复制数字、地址)
- 自定义成员:调用它自己的拷贝构造
- 什么时候需要手动写:
- 无堆内存:默认拷贝够用,不用写
- 有堆内存:默认浅拷贝会让两个对象共用一块堆内存,销毁时释放两次程序崩溃,必须手写深拷贝(重新开一块独立内存)
- 返回值区别:
- 值返回:生成临时对象,调用拷贝构造,效率低
- 引用返回:不拷贝;但不能返回局部对象的引用(局部对象销毁,引用变成野引用)
4.3 基础示例
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int y=2000)
{
_y = y;
}
// 拷贝构造
Date(const Date& d)
{
cout << "执行拷贝构造" << endl;
_y = d._y;
}
void Show()
{
cout << _y << endl;
}
private:
int _y;
};
int main()
{
Date d1(2025);
Date d2(d1); // 拷贝构造
Date d3 = d1; // 拷贝构造
d2.Show();
d3.Show();
return 0;
}
5. 赋值运算符重载(两个现成对象互相复制)
5.1 运算符重载基础
5.1.1 概念
让±*/==++这些运算符支持类对象运算,本质是特殊函数,名字统一为 operator运算符。
5.1.2 基础规则
- 二元运算符写成成员函数,只传1个参数(左边是this对象)
- 运算符优先级、结合顺序不会改变
- 5个运算符不能重载:
.* :: sizeof ?: . - 重载必须带至少一个自定义类型,不能修改int等内置类型运算
- 区分前置++、后置++:后置多加一个没用的int参数区分重载
- 流运算符<<、>>必须写成全局函数,配合友元访问私有变量
5.2 赋值重载 operator=
5.2.1 和拷贝构造核心区别
- 拷贝构造:创建新对象,用旧对象初始化
- 赋值重载:两个已经存在的对象,互相赋值覆盖内容
5.2.2 特点
- 只能写成类内部成员函数,不能全局
- 参数推荐const引用,减少拷贝开销
- 返回值推荐类引用,支持连续赋值 d1=d2=d3,返回*this
- 不手写编译器生成默认赋值,和默认拷贝构造一样是浅拷贝
- 手动写了析构释放堆内存,就必须手动写赋值重载实现深拷贝
5.2.3 简单示例
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int y=2000)
{
_y = y;
}
// 赋值重载
Date& operator=(const Date& d)
{
// 防止自己赋值自己
if(this != &d)
{
_y = d._y;
}
return *this;
}
void Show()
{
cout << _y << endl;
}
private:
int _y;
};
int main()
{
Date d1(2025);
Date d2(1999);
d2 = d1; // 调用赋值重载,两个对象都已经存在
d2.Show();
return 0;
}
6. const成员函数 + 取地址重载
6.1 const成员函数
6.1.1 写法
函数括号后面加const:void Show() const
6.1.2 作用
修饰隐含的this指针,函数内部不允许修改任何成员变量,只做读取操作。
6.1.3 调用规则
- 普通对象:能调用普通函数、const函数
- const常量对象:只能调用const成员函数,不能调用普通成员函数
6.1.4 示例
cpp
#include<iostream>
using namespace std;
class Date
{
public:
Date(int y=2000)
{
_y = y;
}
// const只读函数
void Show() const
{
cout << _y << endl;
}
private:
int _y;
};
int main()
{
Date d1;
const Date d2; // 常量对象
d1.Show();
d2.Show(); // 常量对象只能调用const函数
return 0;
}
6.2 取地址运算符重载
6.2.1 两种重载
- 普通对象取地址:
Date* operator&() - const对象取地址:
const Date* operator&() const
6.2.2 使用场景
绝大多数情况不用自己写,编译器默认实现够用;只有特殊需求(隐藏对象真实地址)才手动改写。
6.2.3 极简示例
cpp
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int _y;
};
7. const写在函数前、后,以及前后都写的区别
7.1 const 在函数后方(成员函数专属)
void Print() const
修饰this指针,函数内部不能修改成员;const对象仅能调用这类函数。
7.2 const 在函数前方(修饰返回值)
const Date Func()
限制函数返回的临时对象不可修改;返回内置类型无意义。
7.3 const 前后都写
const Date Func() const
两层限制同时生效:
- 末尾const:函数内不能修改当前对象成员;
- 开头const:函数返回的对象不能被修改。
类和对象三
1. 构造函数初始化列表
1.1 基础语法
构造函数后加 :,逗号分隔成员 成员(初始值)
cpp
Date(int y)
:_year(y), _month(1)
{}
1.2 三大必须放初始化列表的成员(函数体内赋值报错)
- 引用成员
int& _ref - const常量成员
const int _n - 无默认构造的自定义类型成员
Time _t
1.3 核心规则
- 每个成员在列表只能初始化1次;
- 初始化顺序 = 类内声明顺序,和列表书写顺序无关(极易踩坑);
- 无论写不写初始化列表,所有成员都会先走初始化列表,再进函数体;
- 类内声明处写
int _year = 1是缺省值,仅给未在列表显式初始化的成员使用; - 优先使用初始化列表,避免先默认构造再赋值,提升效率。
1.4 易错示例
cpp
class A
{
public:
A(int a)
:_a1(a), _a2(_a1)
{}
private:
int _a2; // 先初始化(声明在前)
int _a1; // 后初始化
};
// 输出:1 随机值,_a2初始化时_a1还未赋值
2. 隐式类型转换 & explicit关键字
2.1 隐式转换规则
单参/多参带缺省值的构造函数,支持内置类型自动转类对象:
cpp
class A{ A(int x){} };
A a = 1; // 隐式转换,int临时对象构造再拷贝,编译器优化为直接构造
A b = {2,3}; // C++11多参数隐式转换
2.2 explicit作用
修饰构造函数,禁止隐式转换,只能显式调用:
cpp
explicit A(int x){}
A a = 1; // 编译报错
A a(1); // 合法显式构造
3. static 静态成员
3.1 静态成员变量
- 存储在静态区,不属于单个对象,所有对象共享;
- 类内仅声明,必须类外初始化;不能在类内给缺省值;
cpp
class A{ static int _count; };
int A::_count = 0; // 类外初始化
3.2 静态成员函数
- 无隐含
this指针; - 仅能访问静态成员,不能访问普通成员;
- 调用方式:
类名::静态函数()/对象.静态函数();
3.3 访问权限
静态成员同样受 public/protected/private 控制,私有静态外部无法直接访问。
3.4 经典示例:统计对象个数
cpp
class A
{
public:
A(){ ++_cnt; }
A(const A&){ ++_cnt; }
~A(){ --_cnt; }
static int GetCount(){ return _cnt; }
private:
static int _cnt;
};
int A::_cnt = 0;
3.5 经典考题:1+2+...+n
利用变长数组创建n个对象,构造函数累加静态变量:
cpp
class Sum
{
public:
Sum(){ _ret += _i++; }
static int GetRes(){ return _ret; }
private:
static int _i, _ret;
};
int Sum::_i = 1;
int Sum::_ret = 0;
int Sum_Solution(int n)
{
Sum arr[n]; // 创建n个Sum对象,执行n次构造
return Sum::GetRes();
}
4. 友元 friend
4.1 友元函数
- 类内声明
friend 函数名,函数不是类成员; - 可访问类私有/保护成员;不受访问限定符限制;
- 一个函数可作为多个类的友元。
cpp
class A{ friend void func(A& a); };
void func(A& a){ cout << a._a; }
4.2 友元类
friend class B;:B类所有成员函数都能访问当前类私有成员
4.3 两大特性
- 单向:A是B友元 ≠ B是A友元;
- 不可传递:A友元B,B友元C ≠ A友元C;
4.4 优缺点
方便跨类访问,但破坏封装、提高耦合,尽量少用。
5 内部类(嵌套类)
5.1 定义
一个类写在另一个类内部,是独立类,外部类对象不包含内部类。
5.2 核心特点
- 内部类默认是外部类友元,可直接访问外部类私有;
- 内部类受外部类访问限定符控制(写private则外部无法使用);
- 调用格式:
外部类::内部类 变量;
cpp
class A
{
public:
class B{}; // 内部类
private:
static int _k;
};
A::B b;
5.3 使用场景
仅给外部类配套使用的工具类,封装隔离。
6 匿名对象
6.1 定义
类名(参数),无变量名,仅当前一行有效,行结束立即析构。
cpp
A(1); // 匿名对象,执行完本行销毁
Solution().Sum_Solution(10); // 临时调用函数,用完释放
6.2 区分易错
A a(); 是函数声明,不是匿名对象;匿名对象不加变量名。
7 对象拷贝的编译器优化
7.1 优化目的
减少构造+拷贝构造连续调用,消除临时对象,提升效率。
7.2 典型优化场景
- 传参
f1(1):隐式转换直接构造,不生成临时对象; f1(A(2)):匿名对象直接传给函数,省去拷贝;- 返回值
A aa2 = f2();:局部对象直接构造给接收变量,无临时拷贝;
7.3 不优化场景
aa1 = f2();:已有对象赋值,无法合并构造,会产生临时对象。
7.4 关闭优化
编译命令:g++ test.cpp -fno-elide-constructors
7.5 优化差异
- VS2019 Debug:生成多层临时对象,多次拷贝;
- VS2022 Debug/GCC:激进优化,直接构造目标对象,无临时拷贝。