第一章:面向对象核心——类和对象深度讲解

1.类的定义

1.1 类定义格式

class是C++定义类的关键字,类名紧跟关键字,大括号包裹类的全部成员,类定义结束后必须加分号,不可省略。类内部包含两类成员:
· 成员变量:描述类的特征;
· 成员函数:描述类的行为。
行业惯例:为了区分成员变量与局部变量,通常给成员变量前加前缀_或m,该规则仅为编码规范,并非C++语法强制要求。
C++兼容C语言struct语法,同时对struct进行了升级:struct内部可以定义成员函数;C语言需要typedef给结构体重命名,C++中struct的名字可直接作为类型使用。
定义在类里的成员函数默认为inline。
代码示例:

1.2 访问限定符与封装特性

封装是C++面向对象三大特性之首,核心逻辑:将数据与操作数据的方法绑定为一个整体,通过访问权限控制对外暴露的接口,规避非法修改数据的风险。
1.public:类外部可直接访问成员。
2.private:仅类内部成员函数可访问,外部无法直接读写
3.protected:当前仅和private行为一致,继承章节会体现二者差异。
权限作用域:从限定符声明位置开始,至下一个权限限定符为止;若末尾无额外限定符,则作用域延续至类结束。
工程通用编码规范:成员变量一律设置为private/protected,仅对外提供必要的public成员函数作为操作接口
class定义成员没有被访问限定符修饰时默认为private,struct默认为public。

1.3 类域

类定义了⼀个新的作用域,类的所有成员都在类的作用域中。在类体外实现成员函数时,必须使用"::"指明函数所属的类,否则编译器会将函数识别为全局函数,导致编译报错。
声明与定义分离,需要指定类域。代码示例:

2. 实例化

2.1 核心概念

类是一套抽象的模版、声明,仅规定了成员结构,不会分配物理内存,无法存储数据。
实例化:使用类类型在内存中创建具体对象的过程,只有实例化后的对象,才会分配栈/堆内存,用来存储成员变量。
通俗类比:类相当于建筑施工图纸,仅规定房间数量,尺寸,没有实体空间;实例化对象相当于按照图纸建成的房屋,拥有真实可用的空间,可以存放物品(数据)。
一个类可以实例化任意多个独立对象,每个对象拥有专属的成员变量副本。
代码示例:

cpp 复制代码
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;
	Date d2;

	d1.Init(2026, 7, 24);
	d1.Print();

	d2.Init(2026, 11, 7);
	d2.Print();

	return 0;
}

3.对象内存大小与内存对齐规则

3.1 对象内存存储核心结论

我们先来分析:类实例化出来的对象内部究竟存放哪些内容。

类创建的每一个独立对象,都会拥有专属独立内存空间,对象中保存成员变量。那么成员函数是否同样存储在对象内部?

成员函数经过编译后转变为机器指令,统一存放于程序的代码段,并不占用对象的内存空间。

那对象内部会不会保存成员函数的地址指针?

简单推演:Date实例化d1和d2两个对象,d1和d2的成员变量 _year/_month/_day相互独立,各自存储数据;但二者调用的Init,Print成员函数是完全相同的。

倘若每个对象都存储一份函数指针,大量创建对象时,指针就会重复冗余存放,造成极大内存浪费。
补充:普通成员函数的地址在编译链接阶段就可以确定,编译器编译时直接完成函数地址绑定,运行期间不需要查找函数地址。只有实现多态(虚函数)时,才需要在对象内保存虚函数相关地址信息,该内容后续章节讲解。
综上:普通类对象只存储成员变量,不存储普通成员函数。
同时,C++规定,类对象内存布局必须遵守内存对齐规则。

*内存对齐规则

1.第⼀个成员变量,存放在相对于对象起始偏移量为0的地址处。
2.其余成员变量,起始地址必须是对齐数的整数倍。
注意:对齐数 = 编译器默认的⼀个对齐数与该成员变量大小中的较小值。
3.VS中默认的对齐数为8
4.结构体/类总大小为最大对齐数的整数倍。
最大对齐数:所有成员对齐数中的最大值。
5.如果类/结构体中嵌套了其他结构体:嵌套的结构体需要对齐到自己的最大对齐数的整数倍;整体对象总大小,同样需要满足整体最大对齐数的整数倍。
我们用下面这个类进行计算演示:

计算结果:8
内存布局如下:

3.2 为什么要进行内存对齐?

因为读取内存中的数据时,并不是从任意位置开始读任意字节的,而是从固定的整数倍的位置开始读固定字节。
内存对齐本质是用少量空间开销,换取高效的内存访问效率。

3.3空类/无成员变量类的对象大小

思考一下:下列B与C的实例化对象是否等于0?

我们测试后得到:

结论:没有任何成员变量的类,对象大小依旧占用1字节。
原理:C++标准不允许存在大小为0的对象。这个1字节仅仅是占位字节,用来唯一标识这个对象在内存中存在,不存储任何有效数据。

4.this指针

回顾前面的Date类,我们提供了 Init 与 Print 两个成员函数。
成员函数代码中并没有区分不同对象,那当执行d1.Print()、d2.Print()时,函数如何区分当前要操作d1还是d2的数据?
C++通过隐式this指针来解决这个问题。

4.1 this指针底层原理

编译器处理成员函数时,会自动在形参第⼀个位置增加⼀个当前类类型的指针,也就是this
指针。
以Date类的Init函数为例,底层真实原型如下:

cpp 复制代码
void Init(Date* const this, int year, int month, int day)

类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this-
>_year = year;

4.2 this指针语法规则

1.调用成员函数时,编译器自动把调用对象的地址作为实参,传递给this指针

2.不能在函数声明、调用时手动显式填写this指针,由编译器自动处理;

3.可以在函数体内手动书写this->访问成员

4.3 this指针经典练习题(检验理解程度)

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
解析:编译后调用成员函数的本质是call固定的函数地址,成员函数不在对象内存中。
使用对象指针调用成员函数,本质是把指针的值传递给this指针。
本题只完成了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。this是空指针,发生空指针解引用,程序崩溃。
3. this指针存在内存哪个区域的 ()
A. 栈 B.堆 C.静态区 D.常量区 E.对象里面
答案:A
解析:this指针本质是成员函数的形参,形参会存储在栈内。
补充:但是这道题也不完全准确,在vs编译器下,由于this指针的频繁调用,vs就把它放到了寄存器里提高运行效率。

5. C++和C语言实现Stack对比

面向对象三⼤特性:封装、继承、多态。我们通过栈的两种实现方式,初步理解封装。
通过下面两份代码对比可以发现:底层数据逻辑没有发生变化,但是代码组织形式区别巨大。
• C++中数据和函数都放到了类里面,通过访问限定符进行了限制,外部无法随意篡改内部数据。这就是封装最直观的体现:规范化管理数据,规避非法访问。
理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后⾯还需要不断的去学习。
• C++语法更加便捷:成员函数默认隐式接收对象地址(this指针),不需要外部持续传递结构体指针;直接使用类名作为类型,无需typedef。
C语言实现Stack代码:

cs 复制代码
#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 = 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->capacity == ps->top)
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		STDataType* tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("realloc");
			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++实现Stack代码:

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;
 }

6. 类的默认成员函数

默认成员函数就是用户没有显式实现,编译器会自动生成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认生成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。

6.1 构造函数

6.1.1 核心概念

构造函数是特殊的成员函数,替代C语言中的Init初始化函数。对象在栈/堆上实例化时,编译器自动调用对应构造函数完成成员变量初始化。
注意:栈空间在对象定义时就已分配,构造函数不负责开辟对象本身内存。

6.1.2 语法规则

  1. 函数名与类名相同;
  2. 无返回值类型,不需要写void;
  3. 对象实例化时系统会自动调用对应的构造函数;
  4. 支持函数重载:一个类可以定义多组参数列表不同的构造函数;
  5. 如果类中没有显式定义任何构造函数,则C++编译器会自动生成⼀个无参的默认构造函数;只要用户手写任意一个构造函数,编译器不再自动生成默认构造;
  6. 无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数,都叫做默认构造函数。但是这三个函数有且只有⼀个存在,不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载,但是调用时会存在歧义。总结⼀下就是不传实参就可以调用的构造就叫默认构造。

7.编译器自动生成的默认构造,对两类成员处理:
**·**对内置类型成员变量的初始化没有要求,也就是说是是否初始化是不确定的,看编译器。
**·**对于自定义类类型成员变量,自动调用这个成员变量的默认构造函数初始化。如果这个成员变量没有默认构造函数,那么就会报错。
说明:内置类型就是语言提供的原生数据类型, 如:int/char/double/指针等,自定义类型就是我们使用class/struct等关键字自己定义 的类型。
代码示例1:

cpp 复制代码
#include<iostream>
 using namespace std;
 class Date
 {
 public:
	 // 1.⽆参构造函数
		 Date()
		 {
		 _year = 1;
		 _month = 1;
		 _day = 1;
		 }
	
		 // 2.带参构造函数
		 Date(int year, int month, int day)
		 {
		 _year = year;
		 _month = month;
		 _day = day;
		 }
	
		 // 3.全缺省构造函数
		 /*Date(int year = 1, 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()
 {
	 // 如果留下三个构造中的第⼆个带参构造,第⼀个和第三个注释掉
	 // 编译报错:error C2512: "Date": 没有合适的默认构造函数可⽤
	 Date d1; // 调⽤默认构造函数
	 Date d2(2025, 1, 1); // 调⽤带参的构造函数
	
	 // 注意:如果通过⽆参构造函数创建对象时,对象后⾯不⽤跟括号,
     //否则编译器⽆法区分这⾥是函数声明还是实例化对象
		 // warning C4930: "Date d3(void)": 未调⽤原型函数(是否是有意⽤变量定义的?)
		 Date d3();
	
	   d1.Print();
	   d2.Print();
	
	   return 0;
 }

易错点:

"Date d3(); "不会创建局部对象,编译器将其解析为无参函数d3的声明,编译无报错,但运行时无法操作d3对象;无参对象实例化直接写"Date d3;"即可。

代码示例2:动态栈Stack全缺省构造

cpp 复制代码
#include<iostream>
  using namespace std;
  typedef int STDataType;
 class Stack
 {
     public:
	  Stack(int n = 4)
		  {
		  _a = (STDataType*)malloc(sizeof(STDataType) * n);
		  if (nullptr == _a)
		  {
			  perror("malloc申请空间失败");
			  return;
		  }
		 
			  _capacity = n;
		  _top = 0;
		  }
	  // ...
      private:
	  STDataType * _a;
	  size_t _capacity;
	  size_t _top;
 };

代码示例3:复合类成员调用规则

当类中包含自定义类型成员变量时,编译器生成的默认构造会自动依次调用每个成员的默认构造,无需手动初始化:

cpp 复制代码
 // 两个Stack实现队列
	  class MyQueue
	  {
	  public:
		  //编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化
	  private:
		  Stack pushst;
		  Stack popst;
		  };
int main()
{
	  MyQueue mq;
	  return 0;
}

6.2 析构函数

6.2.1 核心概念

析构函数功能与构造函数完全互补,替代C语言Destroy销毁函数。
局部对象存储于栈帧,函数执行结束栈帧销毁时,对象本身内存由操作系统自动回收;但对象内部手动申请的堆内存、文件句柄等资源,必须通过析构函数手动释放。C++规定:对象生命周期终止时,编译器自动调用析构函数完成资源清理。

6.2.2 语法规则

  1. 析构函数名是在类名前加上字符 ~;
  2. 无参数、无返回值 (这里跟构造类似,也不需要加void)
  3. 不支持重载,⼀个类只能有⼀个析构函数;
    4.若未显式定义,系统会自动生成默认的析构函数;
  4. 对象⽣命周期结束时,系统会⾃动调⽤析构函数;
  5. 默认析构处理规则:
    **·**内置类型成员:不执行任何操作
    **·**自定义类类型成员:自动调用成员所属类的析构函数
  6. 还需要注意的是我们显式写析构函数,对于自定义类型成员也会调用他的析构,也就是说自定义类型成员无论什么情况都会自动调用析构函数。
  7. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,如Date;如果默认生成的析构就可以用,也就不需要显式写析构,如MyQueue;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack。
  8. ⼀个局部域的多个对象,C++规定后定义的先析构。
    代码示例:
cpp 复制代码
#include<iostream>
using namespace std;
typedef int STDataType;
 class Stack
 {
   public:
     Stack(int n = 4)
	 {
		 _a = (STDataType*)malloc(sizeof(STDataType) * n);
		 if (nullptr == _a)
		 {
		   perror("malloc申请空间失败");
		   return;
		 }
		
			 _capacity = n;
		     _top = 0;
	 }
	
	~Stack()
	{
		cout << "~Stack()" << endl;
		 free(_a);
		 _a = nullptr;
		 _top = _capacity = 0;
	}
	
 private:
	 STDataType * _a;
	 size_t _capacity;
	 size_t _top;
 };
 // 两个Stack实现队列
 class MyQueue
 {
    public:
	 //编译器默认生成MyQueue的析构函数调用了Stack的析构,释放的Stack内部的资源
		
	 // 显式写析构,也会自动调用Stack的析构
		 /*~MyQueue()
		 {}*/
 private:
	 Stack pushst;
	 Stack popst;
 };
int main()
{
	Stack st;
	MyQueue mq;
	
	return 0;
}

对比⼀下用C++和C实现的Stack解决之前括号匹配问题isValid,我们发现有了构造函数和析构函数确实方便了很多,不会再忘记调用Init和Destory函数了,也方便了不少。

6.3 拷贝构造函数

6.3.1 核心概念

拷贝构造函数是构造函数的一个重载,第⼀个参数是自身类类型的引用,且任何额外的参数都有默认值。

6.3.2 语法规则

  1. 拷贝构造函数的第⼀个参数必须是类类型对象的引用,值传递方式传递形参会使编译器直接报错,因为语法逻辑上会引发无穷递归调用。
    错误写法:
cpp 复制代码
 Date(Date d)
{
	_year = d._year;
	_month = d._month;
	_day = d._day;
}

正确标准写法:

cpp 复制代码
Date(const Date & d)
	 {
		  _year = d._year;
		  _month = d._month;
		  _day = d._day;
	 }

无穷递归调用原理:


2.拷贝构造三大自动调用场景:

C++规定:C++规定自定义类型对象进行拷贝行为必须调用拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。自动调用场景分为三类:

**·**使用同类已有对象初始化新对象:Date d2(d1);/Date d2=d1;

cpp 复制代码
int main()
{

	Date d1(2026, 7, 26);
	//两种写法都会触发拷贝构造
    Date d2(d1);
    Date d3 = d1;
  return 0;
}

**·**函数形参为自定义类、值传递传参:void Func1(Date d),调用Func1时,实参d1拷贝构造形参d

cpp 复制代码
#include<iostream>
using namespace std;

class Date
{
  public:
	 Date(int year = 1, int month = 1, int day = 1)
	 { 
		 _year = year;
		 _month = month;
		 _day = day;
	 }
	 //Date(Date d)
	 Date(const Date & d)
	 {
		  _year = d._year;
		  _month = d._month;
		  _day = d._day;
	 }
	 void Print()
	 {
		  cout << _year << "-" << _month << "-" << _day << endl;
	 }
 private:
	 int _year;
	 int _month;
	 int _day;
};
void Func1(Date d)
{
	 cout << &d << endl;
	 d.Print();
}
int main()
{
   Date d1(2026, 7, 26);
   d1.Print();

   Func1(d1);
}

**·**函数返回值为自定义类,值传递返回:Date func(),返回局部对象时,局部对象拷贝构造临时返回对象

cpp 复制代码
Date func()
{
  Date tmp(2026,7,26);
  return tmp;//返回局部对象,触发拷贝构造临时变量
}
int main()
{
  func();
  return 0;
}

补充:高版本编译器会做返回值优化,该场景下的拷贝构造可能被忽略,调试时可关闭优化查看完整调用流程
3.值传递与引用传参性能对比

cpp 复制代码
// 值传递:每次调用都会触发拷贝构造,产生对象拷贝开销
void Func1(Date d)
{
    cout << &d << endl;
    d.Print();
}

// const引用传参:无对象拷贝,直接绑定原对象,性能更优,工程开发推荐
void Func1(const Date& d)
{
    cout << &d << endl;
    d.Print();
}

建议:自定义类作为函数参数时,统一使用const ClassName&常量左值引用,规避无意义的对象拷贝。
4. 若未显式定义拷贝构造,编译器会自动生成默认拷贝构造函数。
5.默认拷贝构造执行浅拷贝/值拷贝(一个字节一个字节的拷贝):
· 内置基础类型(int、double等):直接完成内存字节拷贝;
· 自定义类类型成员:调用该成员自身的拷贝构造函数。
6.像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显式实现拷贝构造。
而像Stack这样的类,虽然也都是内置类型,但 是_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。

cpp 复制代码
typedef int STDataType;
class Stack
{
  public:
	 Stack(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;
		 }
		
		 ~Stack()
		 {
		   cout << "~Stack()" << endl;
		   free(_a);
		   _a = nullptr;
		   _top = _capacity = 0;
		 }
 private:
	 STDataType * _a;
	 size_t _capacity;
	 size_t _top;
};
int main()
{
	Stack st1;
	st1.Push(1);
	st1.Push(2);
	// Stack不显式实现拷贝构造,⽤⾃动⽣成的拷贝构造完成浅拷贝
	// 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃
	Stack st2(st1);

	return 0;
}

上述代码运行后会直接崩溃,底层原因:

浅拷贝仅复制指针变量的值,st1._a与st2._a存储完全相同的堆地址;函数生命周期结束时,st2、st1依次调用析构,两次执行free(同一块地址),造成程序崩溃。

为了解决这个问题,我们需要进行深拷贝:

加上深拷贝后进行调试,结果如下:


7. 像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的拷贝构造会调用Stack的拷贝构造,也不需要我们显式实现MyQueue的拷贝构造。
这里有⼀个小技巧,如果⼀个类显式实现了析构并释放资源,那么他就需要显式写拷贝构造,否则就不需要。

8. 值返回会产生⼀个临时对象调用拷贝构造;
值引用返回,返回的是返回对象的别名,没有产生拷贝。但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使用引用返回是有问题的,这时的引用相当于⼀个野引用,类似⼀个野指针。

引用返回合法场景:

· 返回全局对象;

· 返回静态局部对象(添加static修饰,存储于静态数据区,生命周期与程序一致)

我们的st1是在main函数中创建的外部对象,即便func2函数执行完毕退出,st1也不会被析构销毁。因此函数返回它的引用是安全的,这段代码不存在问题。

7. 运算符重载

7.1 核心概念

当内置运算符作用于自定义类对象时,C++允许我们通过运算符重载重新定义运算符的行为。编译器识别对象运算表达式时,会自动匹配调用对应的重载函数;若无匹配的重载函数,则直接编译报错。
运算符重载本质是拥有特殊命名规则的函数:函数名固定为operator+待重载运算符,具备标准函数的返回值、形参列表与函数体。

重要规则:

1.一元运算符仅需1个形参;二元运算符需要2个形参。
2.二元运算符调用时,左侧运算对象传递给第一个形参,右侧运算对象传递给第二个形参


7.2 两种重载写法:全局函数vs类成员函数

7.2.1 全局函数重载

以==运算符举例,全局重载需要两个形参,分别接收左右两侧对象:

cpp 复制代码
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//Date(Date d)
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}

	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
//private:
	int _year;
	int _month;
	int _day;
};
bool operator <(Date d1, Date d2)
{
}
bool operator == (Date d1, Date d2)
{
	return d1._year == d2._year
	  && d1._month == d2._month
	  && d1._day == d2._day;
}
int main()
{
	Date x1(2026, 7, 31);
	Date x2(2026, 11, 7);
	////有以下两种方法来调用
	operator==(x1, x2);
	x1 == x2;
}

7.2.2 类成员函数重载

若运算符重载作为类的成员函数,第一个运算对象会默认传给隐式this指针,因此形参数量比运算对象总数少一个。
同样以==为例,仅需1个形参接收右侧对象:

7.3 语法规则

1.运算符重载以后,其优先级、结合性与对应的内置类型运算符保持⼀致。
2.不能创造语法中不存在的全新运算符,比如自定义operator@是非法写法。
3.以下运算符禁止重载:

4.只重载对当前类有实际意义的运算符:例如Date类重载-计算日期差有意义,但重载operator+就无实际价值,无需实现。
5.区分前置++与后置++:二者 函数名都是operator++,C++规定后置++额外增加一个int类型形参,以此构成函数重载区分两种写法。

cpp 复制代码
    // 1. 前置++:无int形参,返回自身引用
    Date& operator++()
    {
        // 日期自增(简化逻辑,只演示重载区分,不做完整日期进位)
        _day++;
        return *this;
    }

    // 2. 后置++:必须加int哑元,仅用于区分函数重载
    Date operator++(int)
    {
        // 先保存当前旧日期
        Date temp = *this;
        // 自身自增
        _day++;
        // 返回未自增前的副本
        return temp;
    }

6.流运算符<</>>必须重载为全局函数
cout<<对象、cin>>对象这类流运算符,不能重载为类成员函数,原因如下:
二元运算符的第一个形参对应运算符左侧操作数;如果将operator<<写为类成员函数,第一个参数固定是隐式this(自定义Date对象),调用语法会强制变成对象<<cout,完全违背我们习惯的cout<<对象写法,可读性极差。
解决方案:将<</>>重载为全局函数,把ostream/istream放在第一个形参位置,自定义类对象作为第二个形参,完美匹配cout<<对象的书写习惯

cpp 复制代码
// 全局重载输出流<<
ostream& operator<<(ostream& out, const Date& d)
{
    out << d._year << "年" << d._month << "月" << d._day << "日";
    return out;
}

8.赋值运算符重载

8.1 核心概念

赋值运算符重载是⼀个默认成员函数,用于完成两个已经存在且创建完成的对象执行拷贝赋值,这里要注意跟拷贝构造区分,拷贝构造用于⼀个对象拷贝初始化给另⼀个尚未构造完成的对象。

8.2 语法规则

  1. C++语法规定,赋值运算符operator=必须重载为成员函数。
    函数形参标准写法为const 类名& 变量,否则传值传参会带来额外的拷贝
cpp 复制代码
class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//Date(Date d)
    //拷贝构造:
	Date(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
	//赋值重载:
	void operator=(const Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1(2026, 7, 31);
	Date d2(2026, 11, 7);
	//赋值重载
	d1 = d2;

	//拷贝构造
	Date d3(d2);
	Date d4 = d2;
}
  1. 有返回值,且建议写成当前类类型引用,引用返回可以提高效率,有返回值目的是为了支持d1=d2=d3这种连续赋值场景。
  2. 没有显式实现时,编译器会自动生成⼀个默认赋值运算符重载,默认赋值运算符重载行为跟默认拷贝构造函数类似,对内置类型成员变量会完成值拷贝/浅拷贝(⼀个字节⼀个字节的拷贝),对自定义类型成员变量会调用他的赋值重载函数。
  3. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器自动生成的赋值运算符重载就可以完成需要的拷贝,所以不需要我们显式实现赋值运算符重载。像Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载,也不需要我们显式实现MyQueue的赋值运算符重载。
    这里还有⼀个小技巧:如果⼀个类显式实现了析构并释放资源,那么他就需要显式写赋值运算符重载,否则就不需要。

9. 取地址运算符重载

9.1 const成员函数

9.1.1 核心概念

将const关键字写在成员函数参数列表后方,该函数即为const成员函数。

9.1.2 底层原理

const修饰的本质是约束成员函数隐自带的this指针:

• 普通成员函数隐式指针:Date* const this,允许读写类内成员变量

• const成员函数隐式指针:const Date* const this

指针指向的对象被const修饰,因此函数内部无法修改类的任何成员变量,仅能读取数据。

9.2 取地址运算符重载

C++ 取地址运算符 & 支持重载,分为普通对象取地址重载与 const 对象取地址重载两个版本。
默认情况下,编译器会为自定义类自动生成这两个重载函数,其原生逻辑是返回当前对象的 this 指针,绝大多数业务场景下无需手动显式定义。

仅在特殊业务需求中,我们才需要手动重载该运算符:例如需要屏蔽外部代码获取真实对象地址、自定义返回虚假地址(如统一返回空指针 nullptr)等场景,此时可以自行实现 operator& 重载函数,篡改地址返回结果。

cpp 复制代码
class Date
{
 public :
 Date* operator&()
 {
 return this;
 // return nullptr;
 }

 const Date* operator&()const
 {
    return this;
 // return nullptr;
 }
 private :
  int _year ; // 年
  int _month ; // ⽉
  int _day ; // ⽇
};

10. 再探构造函数

10.1 初始化列表核心概念

此前构造函数初始化成员变量的方式为函数体内赋值,C++提供了更高效的初始化方法: 构造函数初始化列表
初始化列表的使用方式:⼀个冒号开始,接着是⼀个以逗号分隔的数据成员列表,每个"成员变量"后面跟⼀个放在括号中的初始值或表达式。

注意:每个成员变量在初始化列表中只能出现⼀次,语法理解上初始化列表可以认为是每个成员变量定义初始化的地方。

10.2 语法规则

1.引用成员变量,const成员变量和没有默认构造的类类型变量,必须放在初始化列表位置进行初始化,否则会编译报错。
代码示例:

cpp 复制代码
using namespace std;
class Time
{
    public:
	 Time(int hour)
		 : _hour(hour)
		 {
		    cout << "Time()" << endl;
		 }
 private:
	 int _hour;
};
class Date
{
public:
	Date(int& xx, int year = 1, int month = 1, int day = 1)
		: _year(year)
		, _month(month)
		, _day(day)
		,_n(1)
		,_ref(xx)
		,_t(1)
	{
	}
	void Print()const
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
	//必须在初始化列表进行初始化:
	const int _n;
	int& _ref;
	Time _t;
};

2.C++11支持在成员变量声明的位置给缺省值,这个缺省值主要是给没有显式在初始化列表初始化的成员使用的,若成员在初始化列表显式初始化,优先使用列表内的值,忽略类内缺省值。


初始化完整执行逻辑(核心总结)


  1. 初始化列表中按照成员变量在类中声明顺序进行初始化,跟成员在初始化列表出现的的先后顺序无关。建议声明顺序和初始化列表顺序保持⼀致。



答案:D
解析:

  1. 类内成员声明顺序:_a2 在前、_a1 在后,初始化严格遵循声明顺序;

  2. 先初始化 _a2:初始化列表 _a2(_a1),此时 _a1 尚未初始化,内存为随机脏值;

  3. 后初始化 _a1:列表指定值 1,最终 _a1=1,_a2 为随机值;

  4. 程序输出 1 随机值,对应选项D。

11.类型转换

11.1 隐式类型转换规则

C++允许单参数构造函数触发隐式类型转换:

  1. 内置基础类型可自动隐式转换为自定义类对象,需要有相关内置类型为参数的构造函数。
  2. 构造函数前面加explicit就不再支持隐式类型转换。
  3. 类类型的对象之间也可以隐式转换,需要相应的构造函数支持。
    代码示例:
cpp 复制代码
class A
{
public:
   // 构造函数explicit就不再⽀持隐式类型转换
   // explicit A(int a)
	A(int a)
	{
		_a1 = a;
	}
    //拷贝构造
	A(const A& aa)
	{
		_a1 = aa._a1;
	}
	void Print()
	{
		cout << _a1 << " " << _a2 << endl;
	}
private:
	int _a2 = 2;
	int _a1 = 2;
};
int main()
{
	A aa1(1);
	aa1.Print();

	//隐式类型转换
	// 2构造⼀个A的临时对象,再用这个临时对象拷贝构造aa2
    // 编译器遇到连续构造+拷贝构造->优化为直接构造
	A aa2 = 2;
	aa2.Print();

}

11.2 函数传参场景下的隐式转换

11.2.1 单参数隐式转换

当函数形参为类的常引用const A&时,传入内置数字会触发隐式转换,编译器自动用数字构建临时类对象传入函数。

通过对比两种写法可以明显看到第二种写法更加简洁方便。

11.2.2 多参数隐式转换

12.static成员

12.1 核心概念

用static修饰的成员变量为静态成员变量。

  1. 存储特性:不属于任意单个实例对象,所有类对象共享同一份数据,存储在程序全局静态区,不占用实例对象内存,sizeof(类名) 计算大小时不计入静态成员;

  2. 初始化规则:仅允许在类内声明,类外全局域初始化;

  3. 生命周期:程序启动分配内存,程序结束释放,生命周期贯穿整个程序运行周期。

代码示例:统计程序创建的类对象总数

12.2 语法规则

1.用static修饰的成员函数为静态成员函数,静态成员函数没有this指针,无法访问对象独有的非静态成员变量/非静态成员函数;
2.仅能访问类内所有静态成员(静态变量、静态函数);
3.非静态成员函数拥有this指针,可访问任意的静态成员变量和静态成员函数。
代码示例:静态函数访问非静态成员编译报错

4. 突破类域就可以访问静态成员,可以通过类名::静态成员 或者 对象.静态成员 来访问静态成员变量和静态成员函数。

5. 静态成员也是类的成员,受public、protected、private 访问限定符的限制。
6. 不能在声明位置给静态成员变量给缺省值进行初始化,因为缺省值是构造函数初始化列表的,静态成员变量不属于某个对象,不走构造函数初始化列表。

12.3 经典例题

1. 1+2+...+n 牛客题霸
代码如下:

cpp 复制代码
class Sum
{
public:
  Sum()
  {
    ret+=_i;
    ++_i;
  }
static int GetRet()
  {
    return ret;
  }
private:
  static int _i;
  static int ret;
};

int Sum::_i=1;
int Sum::ret=0;

class Solution {
public:
    int Sum_Solution(int n) 
    {
        Sum a[n];

        return Sum::GetRet();
    }
};

答案:E B
解析:

  1. 全局静态存储区对象(全局变量、函数内static局部对象)构造顺序:程序进入main函数前完成构造;

  2. 栈局部对象(普通定义在函数内的对象):运行至定义代码行时才构造;

  3. 析构顺序与构造顺序完全相反;

  4. 本题构造顺序:全局C → static D → 栈A → 栈B;析构顺序:B → A → D → C,对应答案E、B。

13.友元

13.1 核心概念

友元是C++提供的一种访问授权机制,用于在受控前提下突破类封装的访问权限限制,分为 友元函数友元类 。通过关键字friend完成声明, 友元声明必须置于类的内部

13.2 友元函数

1.外部友元函数可访问类的私有和保护成员,友元函数仅仅是⼀种声明,他不是类的成员函数。
2.友元函数可以在类定义的任何地方声明,不受类访问限定符限制。
3.⼀个函数可以是多个类的友元函数。
4.友元类中的成员函数都可以是另⼀个类的友元函数,都可以访问另⼀个类中的私有和保护成员。

假设有两个类:A、B
我们在 A 里面写一句:friend class B;
含义:把B声明成A的友元类

• 另一个类 = A(被访问的类,有私有成员)

• 友元类 = B(授权允许访问A的类)

• 友元类中的成员函数:B 里的函数 func1、func2...
这些B里的函数全都拥有权限读取/修改A里面private/protected的成员。

  1. 若友元函数形参包含尚未完成定义的类类型,必须对该类进行前置声明,告知编译器类型存在,否则会触发编译报错。
cpp 复制代码
// 需要前置声明,否则A的友元函数声明编译器不认识B
class B;
class A
{
	// 友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _a1 = 1;
	int _a2 = 2;
};
class B
{
	// 友元声明
	friend void func(const A& aa, const B& bb);
private:
	int _b1 = 3;
	int _b2 = 4;
};
void func(const A& aa, const B& bb)
{
	cout << aa._a1 << endl;
	cout << bb._b1 << endl;
}
int main()
{
	 A aa;
	 B bb;
	 func(aa, bb);
	
	 return 0;
}

13.3 友元类

使用friend class 类名将外部类声明为当前类的友元类后,该友元类内所有成员函数,均自动获得当前类私有、保护成员的完整访问权限。
代码示例:

cpp 复制代码
class A
{
	 // 友元声明
	 friend class B;
 private:
	 int _a1 = 1;
	 int _a2 = 2;
};

class B
{
 public:
	 void func1(const A & aa)
	 {
	   cout << aa._a1 << endl;
	   cout << _b1 << endl;
	 }
	
	 void func2(const A & aa)
	 {
	    cout << aa._a2 << endl;
		cout << _b2 << endl;
	 }
 private:
	 int _b1 = 3;
	 int _b2 = 4;
};
int main()
{
	A aa;
	B bb;
	bb.func1(aa);
	bb.func1(aa);
	return 0;
}

友元三大核心限制

  1. 单向性:友元关系不具备交换对称性。若B是A的友元,仅B可访问A私有成员,A无权访问B的私有成员,如需双向授权必须双向书写friend声明;

  2. 不可传递性:友元关系不存在传递逻辑。若A是B的友元、B是C的友元,A不会自动成为C的友元;

  3. 破坏封装性:友元机制打破了面向对象封装隔离原则,提升代码耦合度,工程开发中应尽量减少使用。

14. 内部类

14.1 核心概念

若一个类的完整定义书写于另一个类的类域内部,则该嵌套定义的类称为内部类。
内部类是独立完整的类类型,仅作用域被外部类约束;外部类实例的内存布局中不包含内部类对象,因此计算sizeof(外部类)时,内部类不会占用外部类内存空间。
示例代码:(sizeof验证)

运行结果:4

14.2 内部类工程应用场景

当两类业务逻辑高度耦合、内部类仅为外部类提供配套功能时,可将其设计为外部类的内部类;若将内部类置于外部类私有区段,该内部类将成为外部类专属类型,全局域无法调用,实现封装。
学习内部类之后我们可以得到之前牛客求和题型的第二种解法:

cpp 复制代码
class Solution 
{
private:
   static int _i;
   static int ret;
class Sum
{
public:
  Sum()
  {
    ret+=_i;
    ++_i;
  }
};
public:
    int Sum_Solution(int n) {
        Sum a[n];
        return ret;
    }
};
int Solution::_i=1;
int Solution::ret=0;

15. 匿名对象

15.1 核心概念

采用 类名(实参列表) 语法创建、不绑定任何变量名的临时对象,称为匿名对象;
与之对应,类名 对象名(实参列表) 创建带标识的实例,叫做有名对象。
核心特性:生命周期仅存在于当前代码行

匿名对象属于临时变量,本行代码执行结束后,编译器会立刻调用其析构函数销毁释放资源;

适用场景:仅需临时使用一次对象、无需后续复用的场景,省去命名步骤简化代码。

cpp 复制代码
class A
{
public:
	 A(int a = 0)
		 : _a(a)
	 {
		 cout << "A(int a)" << endl;
	 }
	
	 ~A()
	 {
		 cout << "~A()" << endl;
	 }
private:
	 int _a;
};

class Solution {
public:
   int Sum_Solution(int n)
   {
		 //...
		 return n;
	
   }
};
int main()
{
	 A aa1;
	
		 // 不能这么定义对象,因为编译器⽆法识别下面是一个函数声明,还是对象定义
		 //A aa1();
		
		 // 但是我们可以这么定义匿名对象,匿名对象的特点不用取名字,
		 // 但是他的生命周期只有这一行,我们可以看到下一行他就会自动调⽤析构函数
		 A();
	
		 // 匿名对象在这样场景下就很好用,当然还有⼀些其他使用场景,这个我们以后遇到了再说
		 Solution().Sum_Solution(10);
	
		 return 0;
}
相关推荐
冷咖啡离 我的笨笨3 小时前
数据去重:通过 C# 删除 Excel 中的重复行
开发语言·c#·excel
郝学胜-神的一滴3 小时前
[简化版 GAMES 104] 现代游戏引擎 04:从0到1构建你的游戏世界
c++·游戏·unity·游戏引擎·软件工程·unreal engine
Bryce学亮3 小时前
FileLine,基于 Qt 6 + QML 构建的跨平台文件传输与即时通讯工具
数据库·c++·人工智能·python·qt·github
旖旎夜光3 小时前
LeetCode 991: 坏了的计算器(贪心算法) —— 题解
数据结构·c++·算法·leetcode·贪心算法
夜雪一千3 小时前
Python从HTML提取纯文本、去空格、比对内容是否变化:方案+合理性分析
开发语言·python·html
烂蜻蜓3 小时前
Node.js入门教程(一):初识Node.js——服务端JavaScript的诞生与特点
开发语言·javascript·node.js
旖旎夜光3 小时前
LeetCode 553:最优除法(贪心算法) —— 题解
数据结构·c++·算法·leetcode·贪心算法
北冥you鱼5 小时前
Go语言大数(big.Int)比较大小:原理、方法与最佳实践
开发语言·后端·golang
宁之明起(B服)5 小时前
Java 反序列化漏洞-xmldecoder/SnakeYAM
java·开发语言