一 类

类可以理解为结构体的plus版,相较于结构体,类更加扩展(类不仅可以定义各种变量,也可以定义各种的方法和函数)。

1 类的定义格式

• class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省 略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或 者成员函数。

• 为了区分成员变量,⼀般习惯上成员变量会加⼀个特殊标识,如成员变量前面或者后面加_或者m 开头,注意C++中这个并不是强制的,只是⼀些惯例,具体看公司的要求。

• C++中struct也可以定义类,C++兼容C中struct的用法,同时struct升级成了类,明显的变化是 struct中可以定义函数,⼀般情况下我们还是推荐用class定义类。

 • 定义在类面的成员函数默认为inline。

(1)class是定义类的关键字

注意:和结构体struct不同的是:类中类名就是类型,类型定义对象而结构体中struct  m  两个都是类型。

​
​
​
class Stack
{
 //成员函数
  void Init()
   {}

  void push(int x)
   {}
 
 //成员变量
  int* a;
  int top;
  int capacity;
}

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

​

​

​

调用函数:s1.Init(); 

例如:Strck s1中:Strck是类型,s是对象

(2)面向对象的三大特性

面向对象三大特性:封装  继承   多态(后面两个后面再介绍)

1. 封装
  • 核心思想:将数据(属性)和操作数据的方法(函数)捆绑在一起,形成一个独立的 “类”,并隐藏内部实现细节,只对外暴露必要的接口。
  • 目的
    • 保护数据安全性,防止外部随意修改对象内部状态。
    • 降低代码耦合度,使对象的使用和实现分离。

封装的本质体现了严格的规范管理

封装的特点:

(1)数据和方法封装到了一起,都在类里面

(2)访问限定符(限定的是类外面)

(3)访问限定符

1 定义和使用

• C++⼀种实现封装的方式,⽤类将对象的属性与方法结合在⼀块,让对象更加完善,通过访问权限 选择性的将其接⼝提供给外部的用户使用。

• public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访 问,protected和private是⼀样的,以后继承章节才能体现出他们的区别。

 • 访问权限作用域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为止,如果后⾯没有 访问限定符,作用域就到 } 即类结束。

• class定义成员没有被访问限定符修饰时默认为private,struct默认为public。

 • ⼀般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。

2 三种访问限定符

public(公有)、private(私有)和 protected(保护)

private和protected在现阶段没有区别,在介绍继承时才会介绍区别

 下面使用一下访问限定符:

class Stack
{
public:
	// 成员函数
	void Init(int capacity = 4)
	{
		_a = nullptr;  // malloc
		_top = 0;
		_capacity = capacity;
	}

	void Push(int x)
    {}

private:
	// 成员变量
	int* _a;
	int _top;
	int _capacity;
}

如果调用private中的对象,如:

int main()
{
 s1.top++;
}

编译时会报错:显示无法访问private成员

3 访问限定符的使用

一定要给访问限定符吗?

class定义成员没有被访问限定符修饰时默认为private,struct默认为public

访问限定符的使用区间

访问权限作用域从该访问限定符出现的位置开始直到下⼀个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 }(花括号) 即类结束

 struct的升级

1.在c++中,struct变成了类(当作类使用,内部可以定义函数)

2.类名即类型,用结构体名称就可以指代类型,但是在c中必须加上struct.   在c++中我们还是主要使用class

3 特殊使用struct当作类的情况:类不想使用访问限定符限制且想变成公有,此时用struct(默认公有)

  (4)两种命名规范

C++没有同一的命名规范,因为C++历史太早,在创建的时候还没有命名规范的意识。以下介绍两种使用度较高的命名规范。

1 驼峰命名法

特点:单词之间不使用分隔符,而是通过大写字母区分不同单词,整体呈现类似 “驼峰” 的起伏形态

分类

  • 小驼峰(变量):第一个单词首字母小写,后续单词首字母大写
  • 大驼峰(函数,类型):所有单词首字母均大写
// 小驼峰
int studentAge;                // 变量

// 大驼峰
class StudentInfo;             // 类
struct Point3D;                // 结构体
2. 蛇形命名法
  • 特点:单词之间用下划线 _ 分隔,所有字母小写

int student_age;                // 变量
void calculate_total_score()    // 函数
3 总结

一般情况下,使用驼峰命名法。

在Window系统里,用驼峰命名法。在Linux系统里,用蛇形命名法。

(5)类域

C++中定义了定义了四种域:全局域,局部域,命名空间域,类域

1 类域的定义

• 类定义了⼀个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使⽤  ::  作 用域操作符指明成员属于哪个类域。

• 类域影响的是编译的查找规则,下⾯程序中Init如果不指定类域Stack,那么编译器就把Init当成全局函数,那么编译时,找不到array等成员的声明/定义在哪里,就会报错。指定类域Stack,就是知道Init是成员函数,当前域找不到的array等成员,就会到类域中去查找。

2 类域的使用

 类域中允许声明和定义分离(注意:缺省参数只能在声明中),且需要指定类域

​
// 声明和定义分离,需要指定类域 
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();
}

除了局部和全局,也要到类里面去找。

注意:类里面定义,既会向上找。也会向下找,即在整个类中寻找。但是在结构体中只会向上查找

2 类的实例化

(1)如何区分什么时候是申明和定义

申明和定义是两个易混淆的概念:

声明

  • 核心作用:告诉编译器 “这个实体存在”,说明其名称、类型等基本信息,但不分配内存,也不提供具体实现
  • 目的:让编译器知道如何识别和处理这个实体(如变量的类型、函数的参数和返回值),避免 “未声明的标识符” 错误。
  • 特点:一个实体可以被多次声明

2. 定义

  • 核心作用:不仅声明实体的存在,为其分配内存(变量)或提供具体实现(函数、类等)。
  • 目的:让实体真正 “可被使用”,编译器需要知道其具体内容才能生成对应的机器码。
  • 特点:一个实体在同一作用域内只能被定义一次(否则会导致 “重定义” 错误)。

class Stack
{
  public:
  //成员函数
  void Init(int n = 4)
  {}
  //成员变量  声明(没有开辟空间)
  int* array;
  size_t capacity;
  size_t top;
}

int main()
{
 //定义  类实例化对象
 Strack s1;

return 0;
}
(2)实例化概念

用类类型在物理内存中创建对象的过程,称为类实例化出对象。
• 类是对象进行一种抽象描述,是⼀个模型一样的东西,限定了类有哪些成员变量,这些成员变量只是声明,没有分配空间,用类实例化出对象时,才会分配空间。
• 一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。

打个比方:类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,设计图规划了有多 少个房间,房间大小功能等,但是并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图⼀样,不能存储数据,实例化出的对象分配物理内存存储数据。

一个类可以对应多个实例化对象

 (3)对象大小
1 sizeof既可以传对象也可以传类型

1. 底层逻辑:sizeof 本质是计算 “类型大小”

无论传入的是对象 / 变量还是类型sizeof 最终计算的都是该操作数对应的数据类型的大小

  • 当传入类型(如 intdoubleMyClass)时,直接计算该类型的大小。
  • 当传入对象 / 变量(如 aobj)时,编译器会先推导出该对象的类型,再计算其类型的大小(与对象的具体值无关)。

传入类型:

  cout << sizeof(int) << endl;      // 计算int类型的大小(通常4字节)
  cout << sizeof(double) << endl;   // 计算double类型的大小(通常8字节)

传入对象/变量:

2 内存对齐

类与c语言中的结构体相似,都要遵从内存对齐

什么是内存对齐:

内存对齐是计算机系统中为了提高内存访问效率而对数据在内存中的存储位置所做的一种约束 ——数据的起始地址必须是某个特定值(通常是数据大小的整数倍)的倍数

内存对齐规则:

• 第⼀个成员在与结构体偏移量为0的地址处。

 • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

• 注意:对齐数 = 编译器默认的⼀个对齐数与该成员大小的较小值。

• VS中默认的对齐数为8。

• 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。

• 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。  

 3 对象大小

对象大小只考虑成员变量,不考虑成员函数

示例:计算下列三个类的对象大小

class A
{
  public:
	void Print()
	{
		cout << _ch << endl;
	}
 private:
	char _ch;
	int _i;
 };

class B
{
 public:
	void Print()
	{
		//...
	}
};

class C
{};

int main()
{
	cout << sizeof(A) << endl;
	// 开1byte为了占位,不存储实际数据,表示对象存在过
	cout << sizeof(B) << endl;
	cout << sizeof(C) << endl;

  return 0;
}

classA: 8

A 包含两个成员变量:char _ch(1 字节)和 int _i(4 字节)。
根据内存对齐规则:

  • _ch 占据 1 字节(地址 0)。

  • _i 需对齐到 4 字节边界,因此在 _ch 后填充 3 字节(地址 1~3),_i 从地址 4 开始存储(4 字节)。

  • 总大小为 1 + 3(填充) + 4 = 8 字节,且 8 是最大成员对齐值(4)的倍数,无需额外填充。

classB:  1

B 是包含成员函数的非空类,但成员函数不占用对象的内存空间(函数代码存储在代码段,而非对象的内存中)。
C++ 规定:任何类的实例都必须有唯一地址,因此编译器会为无成员变量的类分配 1 字节的 “占位空间”,确保每个对象有独立地址。

classC:  1

C 是完全空的类(无成员变量和成员函数),但同样遵循 “每个对象必须有唯一地址” 的规则。
编译器会为其分配 1 字节的占位空间,因此大小为 1 字节。

那么可能会出现疑问,像B,C这样没有内容的类真的存在吗?

答案是:存在。在Leetcode的OJ题中很常见,如下图。在我们后面会学到的伪函数也是这种情况(只存在成员函数,不存在成员变量)

3 this  指针

 定义

在面向对象编程中,this指针(或this引用)是一个特殊的关键字,用于在类的成员函数中引用当前正在操作的对象实例。它是编译器自动维护的隐含指针,确保成员函数能正确识别和操作调用它的对象的数据。

Date类中有 Init 与 Print 两个成员函数,函数体中没有关于不同对象的区分,那当d1调用Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这里就要看到C++给了⼀个隐含的this指针解决这里的问题
• 编译器编译后,类的成员函数默认都会在形参第⼀个位置,增加⼀个当前类类型的指针,叫做this指针。比如Date类的Init的真实原型为, void Init(Date* const this, int year, 
int month, int day)
• 类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值, this->_year = year;
• C++规定不能在实参和形参的位置显示的写this指针(编译时编译器会处理),但是可以在函数体内显示使用this指针

 示例
​
class Date
{
  public:
    //void Init(Date* const this, int year, int month, int day)
	void Init(int year, int month, int day)
	{
		//cout << this << endl;

		// const保护this不能修改
		// this = nullptr;

		//this->_year = year;
		this->_year = year;
		this->_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(&d1, 2025, 7, 31);
	d1.Init(2025, 7, 31);

	// d2.Init(&d2, 2025, 7, 31);
	d2.Init(2025, 9, 1);

	d1.Print();
	d2.Print();

	return 0;
}

​

我们来一步一步拆分这个代码

Data部分:

 (1)  public:

  • 成员函数Init()的实际参数列表隐含了Date* const this,即编译器会自动为成员函数添加一个指向当前对象的指针参数。

const在类型Data*的左边,表示const修饰的是this指针本身,如果是const Data* this,const修饰this指针指向的对象。

注意:const在*的左边时才会有权限的放大和缩小。不要混淆概念

  • Init()函数中:在函数中,可以显示使用this指针

​
this->_year = year;  // 显式使用this指针访问成员变量
_month = month;      // 隐式使用this指针(等价于this->_month)

​

main部分:

当执行

Date d1, d2;
d1.Init(2025, 7, 31); 
d2.Init(2025, 9, 1);  

编译器实际处理为

 d1.Init(&d1, 2025, 7, 31);
 d2.Init(&d2, 2025, 9, 1);
  • 调用d1.Init()时,this指针指向d1对象
  • 调用d2.Init()时,this指针指向d2对象
  • 这样就能确保不同对象的成员变量被正确初始化,不会相互混淆

程序运行后:输出

2025/7/31
2025/9/1

这表明d1d2两个对象各自保存了不同的日期数据,this指针正确区分了两个对象的成员。

this指针的显示使用我们会在后面学到

例题

(1)

 正确答案:C

编译错误指的是语法错误。而空指针和野指针是运行逻辑错误。编译器不会对空指针和野指针编译报错,最多给一个警告。

空指针调用成员函数是否安全,取决于函数内部是否通过 this 指针解引用访问对象的非静态成员(非静态成员函数是指属于类的对象实例的成员函数)。只要没有解引用操作(即不访问成员变量),即使 this 是nullptr,调用也可能正常执行。

如果把上面main函数的p->Print()  改成(*p).Print()   答案还是C

在 C++ 中,p->Print() 和 (*p).Print() 本质上是等价的。

  • (*p) 看起来是对指针解引用,但在 C++ 语法中,当使用 . 运算符调用成员函数时,编译器最终仍会将其转换为通过 this 指针调用的形式(即本质还是传递 p 作为 this 指针)。
  • 无论写成 p->Print() 还是 (*p).Print(),如果 Print() 函数内部访问了非静态成员变量(如 _a),都会触发对空指针 p 的解引用(通过 this->_a),这属于未定义行为,会导致程序崩溃。

因此,两种写法在这种场景下没有本质区别,结果相同。

(2)

正确答案:B

 解析: main  函数中 p 是指向 A 类的空指针,调用 p->Print() 时,Print 函数里有 cout << _a << endl;,这会通过 this 指针(此时为 nullptr )访问 _a,属于对空指针解引用,会导致运行崩溃 。

成员对象前会有一个隐藏的this指针

(3)

正确答案:A

Logo

纵情码海钱塘涌,杭州开发者创新动! 属于杭州的开发者社区!致力于为杭州地区的开发者提供学习、合作和成长的机会;同时也为企业交流招聘提供舞台!

更多推荐