目录
[1.1 通俗概念](#1.1 通俗概念)
[1.2 代码对比](#1.2 代码对比)
[二、类的引入(C vs C++ 结构体)](#二、类的引入(C vs C++ 结构体))
[2.1 通俗概念](#2.1 通俗概念)
[三、类的定义(两种方式 + 命名规则)](#三、类的定义(两种方式 + 命名规则))
[3.1 通俗概念](#3.1 通俗概念)
[3.2 两种定义方式](#3.2 两种定义方式)
[方式 1:声明和定义都在类内(适合短小函数)](#方式 1:声明和定义都在类内(适合短小函数))
[方式 2:声明在.h 文件,定义在.cpp 文件(推荐)](#方式 2:声明在.h 文件,定义在.cpp 文件(推荐))
[3.3 命名规则(避坑重点)](#3.3 命名规则(避坑重点))
[正确示例(加前缀 / 后缀区分)](#正确示例(加前缀 / 后缀区分))
[3.4 注意点](#3.4 注意点)
[4.1 通俗概念](#4.1 通俗概念)
[4.2 三种访问限定符](#4.2 三种访问限定符)
[4.3 封装特性(面向对象三大特性之一)](#4.3 封装特性(面向对象三大特性之一))
[面试题:C++ 中 struct 和 class 的区别](#面试题:C++ 中 struct 和 class 的区别)
[5.1 通俗概念](#5.1 通俗概念)
[六、类的实例化(类 vs 对象)](#六、类的实例化(类 vs 对象))
[6.1 通俗概念](#6.1 通俗概念)
[7.1 通俗概念](#7.1 通俗概念)
[7.2 内存对齐规则(核心重点)](#7.2 内存对齐规则(核心重点))
[7.3 注意点](#7.3 注意点)
[八、this 指针(特性 + 面试题)](#八、this 指针(特性 + 面试题))
[8.1 通俗概念](#8.1 通俗概念)
[8.2 this 指针的五大特性](#8.2 this 指针的五大特性)
[8.3 面试题(this 指针经典题)](#8.3 面试题(this 指针经典题))
[题目 1:以下代码运行结果是?(A. 编译报错 B. 运行崩溃 C. 正常运行)](#题目 1:以下代码运行结果是?(A. 编译报错 B. 运行崩溃 C. 正常运行))
[题目 2:以下代码运行结果是?(A. 编译报错 B. 运行崩溃 C. 正常运行)](#题目 2:以下代码运行结果是?(A. 编译报错 B. 运行崩溃 C. 正常运行))
[8.4 注意点](#8.4 注意点)
[九、C 与 C++ 实现 Stack 的对比](#九、C 与 C++ 实现 Stack 的对比)
[9.1 代码对比(核心差异)](#9.1 代码对比(核心差异))
[(1)C 语言实现(数据与方法分离)](#(1)C 语言实现(数据与方法分离))
[(2)C++ 实现(数据与方法封装)](#(2)C++ 实现(数据与方法封装))
[9.2 核心差异对比](#9.2 核心差异对比)
[9.3 注意点](#9.3 注意点)
一、面向过程与面向对象初步认识
1.1 通俗概念
- 面向过程(C 语言):关注「步骤」,比如洗衣服要 "放水→放衣服→搓洗→换水→拧干→晾晒",一步步按流程来,全程盯着操作。
- 面向对象(C++):关注「角色」,把洗衣服拆成 "人、衣服、洗衣粉、洗衣机"4 个对象,人不用管洗衣机怎么转,只需让对象们配合完成,不用纠结内部细节。
1.2 代码对比
面向过程
cpp
#include <stdio.h>
// 步骤函数:放水、放衣服、洗衣、甩干、晾晒
void FillWater() { printf("放水\n"); }
void PutClothes() { printf("放衣服和洗衣粉\n"); }
void Wash() { printf("洗衣机搅拌清洗\n"); }
void SpinDry() { printf("甩干\n"); }
void HangClothes() { printf("晾晒\n"); }
int main() {
// 按步骤调用,全程关注流程
FillWater();
PutClothes();
Wash();
SpinDry();
HangClothes();
return 0;
}
面向对象
cpp
#include <iostream>
using namespace std;
// 定义对象(类)
class Person {
public:
void OperateWashingMachine() { cout << "人操作洗衣机" << endl; }
};
class WashingMachine {
public:
void Wash() { cout << "洗衣机搅拌清洗" << endl; }
void SpinDry() { cout << "洗衣机甩干" << endl; }
};
class Clothes {
public:
void BeWashed() { cout << "衣服被清洗" << endl; }
};
int main() {
// 创建对象,靠对象交互
Person p;
WashingMachine wm;
Clothes c;
p.OperateWashingMachine();
c.BeWashed();
wm.SpinDry();
cout << "人晾晒衣服" << endl;
return 0;
}
cpp
人操作洗衣机
衣服被清洗
洗衣机甩干
人晾晒衣服
注意点
- C++ 兼容面向过程写法,是「基于面向对象」而非纯面向对象;
- 面向对象三大特性:封装、继承、多态(本章重点讲封装);
- 面试小提示:常考 "面向过程与面向对象的区别",核心答 "关注重点不同(步骤 vs 对象)、数据与方法关系不同(分离 vs 封装)"。
二、类的引入(C vs C++ 结构体)
2.1 通俗概念
C 语言的结构体只能放变量,C++ 的结构体(struct)既能放变量也能放函数,相当于 "增强版结构体"。C++ 更推荐用class关键字定义类,功能和 struct 一致,仅默认访问权限不同。
cpp
#include <iostream>
#include <cstdlib>
#include <cassert>
using namespace std;
typedef int DataType;
// C++结构体:可定义变量+函数
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) {
assert(_size < _capacity); // 防止越界
_array[_size] = data;
++_size;
}
// 取栈顶元素
DataType Top() {
assert(_size > 0); // 栈空不可访问
return _array[_size - 1];
}
// 销毁栈
void Destroy() {
if (_array) {
free(_array);
_array = nullptr;
_capacity = 0;
_size = 0;
}
}
// 成员变量
DataType* _array; // 栈数组
size_t _capacity; // 容量
size_t _size; // 有效元素个数
};
int main() {
Stack s;
s.Init(10); // C++可直接通过对象调用函数(C语言不行)
s.Push(1);
s.Push(2);
s.Push(3);
cout << "栈顶元素:" << s.Top() << endl; // 输出3
s.Destroy();
return 0;
}
cpp
栈顶元素:3
注意点
- C 语言中不能写
s.Init(10),需写StackInit(&s, 10),还要手动传递结构体指针; - C++ 中 struct 和 class 的核心区别:默认访问权限(struct 默认 public,class 默认 private);
- 开发中用 class 定义类更规范,struct 主要用于兼容 C 语言代码。
三、类的定义(两种方式 + 命名规则)
3.1 通俗概念
类就是 "自定义类型的模板",里面可以放变量(属性)和函数(方法),用class关键字定义,结尾必须加;(容易忘!)
3.2 两种定义方式
方式 1:声明和定义都在类内(适合短小函数)
cpp
#include <iostream>
using namespace std;
class Person {
public:
// 成员函数:类内定义(编译器可能当内联函数)
void ShowInfo() {
cout << _name << " " << _sex << " " << _age << endl;
}
public:
// 成员变量:加前缀_区分形参(避坑重点)
char* _name; // 姓名
char* _sex; // 性别
int _age; // 年龄
};
int main() {
Person p;
p._name = (char*)"张三";
p._sex = (char*)"男";
p._age = 20;
p.ShowInfo(); // 输出:张三 男 20
return 0;
}
方式 2:声明在.h 文件,定义在.cpp 文件(推荐)
(1)头文件 Person.h(声明)
cpp
#ifndef PERSON_H // 防止头文件重复包含(必加)
#define PERSON_H
#include <iostream>
using namespace std;
class Person {
public:
// 成员函数声明
void ShowInfo();
public:
// 成员变量声明
char* _name;
char* _sex;
int _age;
};
#endif // PERSON_H
(2)源文件 Person.cpp(定义)
cpp
#include "Person.h"
// 成员函数定义:必须加Person::(作用域限定符)
void Person::ShowInfo() {
cout << _name << " " << _sex << " " << _age << endl;
}
(3)主文件 main.cpp(调用)
cpp
#include "Person.h"
int main() {
Person p;
p._name = (char*)"李四";
p._sex = (char*)"女";
p._age = 18;
p.ShowInfo(); // 输出:李四 女 18
return 0;
}
3.3 命名规则(避坑重点)
错误示例(形参和成员变量冲突)
cpp
class Date {
public:
void Init(int year) {
year = year; // 错误:编译器分不清是形参还是成员变量
}
private:
int year;
};
正确示例(加前缀 / 后缀区分)
cpp
// 方式1:加前缀_(最常用)
class Date {
public:
void Init(int year) {
_year = year; // 清晰区分
}
private:
int _year;
};
// 方式2:加后缀m(公司规范)
class Date {
public:
void Init(int year) {
mYear = year;
}
private:
int mYear;
};
3.4 注意点
- 类结尾的
;不能漏,漏了会编译报错; - 类内定义的长函数不推荐,编译器可能不视为内联,建议分离定义;
- 头文件必须加 "防止重复包含" 宏(
#ifndef/#define/#endif),否则多文件包含会报 "重定义" 错误。
四、访问限定符及封装特性
4.1 通俗概念
访问限定符就是给类的成员 "设权限":有的成员能外部访问(比如手机开机键),有的不能(比如手机内部主板),这就是封装的核心 ------ 隐藏细节,只暴露接口。
4.2 三种访问限定符
cpp
#include <iostream>
using namespace std;
class Person {
public:
// 公有成员:类外可访问(接口)
void ShowInfo() {
// 类内可访问所有成员(公有、保护、私有)
cout << _name << " " << _age << endl;
}
protected:
// 保护成员:类外不可访问(后续继承用)
char* _name = (char*)"王五";
private:
// 私有成员:类外不可访问(隐藏数据)
int _age = 25;
};
int main() {
Person p;
p.ShowInfo(); // 正确:public成员可访问,输出:王五 25
// 错误示例1:访问protected成员
// p._name = (char*)"赵六"; // 编译报错:无法访问protected成员
// 错误示例2:访问private成员
// p._age = 30; // 编译报错:无法访问private成员
return 0;
}
cpp
王五 25
4.3 封装特性(面向对象三大特性之一)
通俗解释
封装就像手机:用户只需用 "开机键、屏幕、充电口"(public 接口),不用管内部 CPU、电池怎么工作(private/protected 细节),厂商把内部零件藏起来,只暴露必要接口,既安全又方便。
面试题:C++ 中 struct 和 class 的区别
标准答案
- 兼容 C 语言:struct 可当结构体用,class 只能定义类;
- 默认访问权限:struct 默认 public,class 默认 private;
- 继承和模板参数:后续讲解(struct 默认 public 继承,class 默认 private 继承)。
注意点
- 访问限定符只在「编译时」有用,编译后内存中没有权限区别;
- 开发建议:成员变量设为 private/protected,通过 public 成员函数(比如 GetName、SetAge)访问,防止误修改;
- 面试小提示:"什么是封装?"------ 答:将数据和操作数据的方法结合,隐藏细节,仅暴露接口供交互。
五、类的作用域
5.1 通俗概念
类就像一个 "专属文件夹",里面的成员(变量、函数)都在这个文件夹里。如果要在文件夹外面用里面的函数,得告诉编译器这个函数属于哪个文件夹(用::作用域限定符)。
cpp
#include <iostream>
using namespace std;
class Person {
public:
// 成员函数声明(在Person这个"文件夹"里)
void PrintPersonInfo();
private:
char _name[20] = "孙七";
char _gender[3] = "男";
int _age = 30;
};
// 类外定义成员函数:必须加Person::(指明所属"文件夹")
void Person::PrintPersonInfo() {
cout << _name << " " << _gender << " " << _age << endl;
}
// 错误示例:不加类名::,编译器以为是全局函数
// void PrintPersonInfo() {
// cout << _name << endl; // 编译报错:_name未声明
// }
int main() {
Person p;
p.PrintPersonInfo(); // 正确:输出 孙七 男 30
return 0;
}
cpp
孙七 男 30
注意点
- 类内成员相互访问不用加限定符,类外必须加
类名::; - 若全局函数与类成员函数同名,用
::函数名访问全局函数,用对象.函数名访问成员函数; - 类的作用域独立,成员变量名可与全局变量名同名(优先访问类内成员)。
六、类的实例化(类 vs 对象)
6.1 通俗概念
类是 "设计图",比如建筑设计图,只画了房子的结构,没有实际空间;对象是 "按设计图盖的房子",有实际空间,能住人。用类创建对象的过程,就是实例化。
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() {
// 错误示例1:直接给类的成员变量赋值(类是设计图,没有空间)
// Date::_year = 2024; // 编译报错:语法错误
// 正确:实例化对象(按设计图盖房子)
Date d1, d2;
d1.Init(2024, 5, 20); // 给d1对象赋值
d2.Init(2024, 5, 21); // 给d2对象赋值
d1.Print(); // 输出:2024-5-20
d2.Print(); // 输出:2024-5-21
// 验证对象占空间(仅存储成员变量)
cout << "Date对象大小:" << sizeof(d1) << endl; // 输出12(3个int,每个4字节)
return 0;
}
cpp
2024-5-20
2024-5-21
Date对象大小:12
注意点
- 一个类可以实例化多个对象,每个对象的成员变量独立存储(值不同),成员函数共用(存在公共代码区);
- 类本身不占内存,只有实例化后的对象才占内存,存储成员变量;
- 面试小提示:"类和对象的关系?"------ 答:类是对象的模板,对象是类的实例,类定义属性和方法,对象存储数据并执行方法。
七、类对象大小的计算(内存对齐)
7.1 通俗概念
对象的大小只算成员变量的大小,成员函数存在公共代码区不算。但成员变量的存储要遵循 "内存对齐" 规则,就像摆箱子,大箱子旁边不能塞小碎片,要按固定大小对齐,提高访问效率。
cpp
#include <iostream>
using namespace std;
// 类1:有成员变量和成员函数
class A1 {
public:
void f1() {}
private:
int _a; // 4字节
};
// 类2:仅有成员函数
class A2 {
public:
void f2() {}
};
// 类3:空类(无成员)
class A3 {};
int main() {
cout << "sizeof(A1) = " << sizeof(A1) << endl; // 输出4
cout << "sizeof(A2) = " << sizeof(A2) << endl; // 输出1
cout << "sizeof(A3) = " << sizeof(A3) << endl; // 输出1
return 0;
}
结果分析
- A1:只算
_a的 4 字节,成员函数 f1 不算; - A2:无成员变量,但编译器给 1 字节(唯一标识这个类的对象,避免大小为 0);
- A3:空类,编译器同样分配 1 字节(原因同上)。
7.2 内存对齐规则(核心重点)
- 第一个成员放在偏移量为 0 的位置;
- 其他成员要对齐到 "对齐数" 的整数倍(对齐数 = 编译器默认对齐数(VS 为 8)和成员大小的较小值);
- 对象总大小是 "最大对齐数" 的整数倍;
- 嵌套结构体:嵌套的结构体对齐到自己的最大对齐数,总大小是所有最大对齐数的整数倍。
cpp
#include <iostream>
using namespace std;
class A {
private:
char _c1; // 大小1,对齐数1,偏移量0
int _i; // 大小4,对齐数4,偏移量4(1的下一个4的倍数)
char _c2; // 大小1,对齐数1,偏移量8(4+4=8)
};
int main() {
cout << "sizeof(A) = " << sizeof(A) << endl; // 输出12
return 0;
}
计算过程
- 实际占用:1(_c1)+3(填充)+4(_i)+1(_c2)+3(填充)=12;
- 最大对齐数是 4(_i 的对齐数),12 是 4 的整数倍,符合规则。
7.3 注意点
- 内存对齐是 "空间换时间",牺牲部分内存提高 CPU 访问效率;
- 空类大小为 1 字节,仅用于标识对象,不存储实际数据;
- 面试小提示:"为什么要内存对齐?"------ 答:CPU 按固定大小读取内存,对齐后无需拆分数据,提高访问效率。
八、this 指针(特性 + 面试题)
8.1 通俗概念
每个非静态成员函数都藏着一个 "隐形指针",叫 this 指针,它自动指向调用这个函数的对象。比如 d1 调用 Init 函数时,this 就指向 d1,函数里操作的成员变量,其实都是 this 指针指向的对象的变量。
cpp
#include <iostream>
using namespace std;
class Date {
public:
// 编译器实际处理:void Init(Date* const this, int year, int month, int day)
void Init(int year, int month, int day) {
// 编译器实际处理:this->_year = year;
_year = year;
_month = month;
_day = day;
}
// 编译器实际处理:void Print(Date* const this)
void Print() {
// 编译器实际处理:cout << this->_year << "-" << this->_month << "-" << this->_day << endl;
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
Date d1, d2;
// 编译器实际处理:d1.Init(&d1, 2024, 5, 20);
d1.Init(2024, 5, 20);
// 编译器实际处理:d2.Init(&d2, 2024, 5, 21);
d2.Init(2024, 5, 21);
d1.Print(); // 输出:2024-5-20
d2.Print(); // 输出:2024-5-21
return 0;
}
8.2 this 指针的五大特性
- 类型:
类类型* const(比如Date* const),不能给 this 赋值(不能改指向); - 只能在非静态成员函数内部使用;
- 是函数形参,存在栈上(VS 用 ecx 寄存器传递),对象中不存储;
- 自动传递,用户不用手动写参数;
- 可在函数体内显式使用(比如
cout << this << endl;)。
8.3 面试题(this 指针经典题)
题目 1:以下代码运行结果是?(A. 编译报错 B. 运行崩溃 C. 正常运行)
cpp
class A {
public:
void Print() {
cout << "Print()" << endl;
}
private:
int _a;
};
int main() {
A* p = nullptr;
p->Print();
return 0;
}
答案:C. 正常运行
分析:
p->Print()调用的是成员函数,函数在公共代码区,不用访问 p 指向的内存;- 函数体内没用到成员变量(没操作
this->_a),this 为空不影响,所以正常运行。
题目 2:以下代码运行结果是?(A. 编译报错 B. 运行崩溃 C. 正常运行)
cpp
class A {
public:
void PrintA() {
cout << _a << endl;
}
private:
int _a;
};
int main() {
A* p = nullptr;
p->PrintA();
return 0;
}
答案:B. 运行崩溃
分析:
- 函数体内访问
_a,本质是this->_a,而 this 指针为空(p 是 nullptr); - 访问空指针指向的内存,触发内存错误,运行崩溃。
8.4 注意点
- this 指针不能为空,但若函数体内不访问成员变量,即使 this 为空也不会崩溃;
- 静态成员函数没有 this 指针(后续讲解),不能访问非静态成员变量;
- 面试小提示:"this 指针存在哪里?"------ 答:作为函数形参,存在栈上(VS 中通过 ecx 寄存器传递)。
九、C 与 C++ 实现 Stack 的对比
9.1 代码对比(核心差异)
(1)C 语言实现(数据与方法分离)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int DataType;
typedef struct Stack {
DataType* array;
int capacity;
int size;
} Stack;
// 初始化:需手动传递Stack*
void StackInit(Stack* ps) {
assert(ps); // 必须检测ps不为空
ps->array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == ps->array) perror("malloc失败");
ps->capacity = 3;
ps->size = 0;
}
// 入栈:需传递Stack*和数据
void StackPush(Stack* ps, DataType data) {
assert(ps);
// 扩容(简化)
if (ps->size == ps->capacity) {
ps->array = (DataType*)realloc(ps->array, ps->capacity * 2 * sizeof(DataType));
ps->capacity *= 2;
}
ps->array[ps->size++] = data;
}
int main() {
Stack s;
StackInit(&s); // 手动传地址
StackPush(&s, 1);
printf("栈顶:%d\n", s.array[s.size - 1]); // 输出1
return 0;
}
(2)C++ 实现(数据与方法封装)
cpp
#include <iostream>
#include <cstdlib>
#include <cassert>
using namespace std;
typedef int DataType;
class Stack {
public:
// 初始化:无需传指针,编译器自动传递this
void Init() {
_array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == _array) perror("malloc失败");
_capacity = 3;
_size = 0;
}
// 入栈:直接访问成员变量,无需传指针
void Push(DataType data) {
CheckCapacity();
_array[_size++] = data;
}
// 取栈顶:直接返回,无需传指针
DataType Top() {
assert(_size > 0);
return _array[_size - 1];
}
private:
// 扩容:私有函数,隐藏实现细节
void CheckCapacity() {
if (_size == _capacity) {
_array = (DataType*)realloc(_array, _capacity * 2 * sizeof(DataType));
_capacity *= 2;
}
}
// 成员变量:私有,防止误修改
DataType* _array;
int _capacity;
int _size;
};
int main() {
Stack s;
s.Init(); // 直接调用,无需传地址
s.Push(1);
cout << "栈顶:" << s.Top() << endl; // 输出1
return 0;
}
9.2 核心差异对比
|-------|------------------------|--------------------|
| 特性 | C 语言实现 | C++ 实现 |
| 数据与方法 | 分离(结构体存数据,独立函数操作) | 封装(类内结合) |
| 参数传递 | 手动传结构体指针,需检测非空 | 自动传 this 指针,用户无需关心 |
| 安全性 | 成员变量可直接修改(如s.size=100) | 成员变量私有,仅通过接口访问 |
| 实现细节 | 扩容等函数对外暴露 | 私有函数隐藏细节,仅暴露必要接口 |
9.3 注意点
- C++ 的封装让代码更简洁、安全,减少指针操作错误;
- C++ 兼容 C 语言写法,但开发中优先用类的封装特性;
- 面试小提示:"C++ 相比 C 语言实现 Stack 的优势?"------ 答:封装数据和方法、自动传递 this 指针、隐藏实现细节、提高代码安全性和可读性。
十、学习总结与建议
- 核心逻辑:本章围绕 "封装" 展开,类是 C++ 封装的核心,通过 "数据 + 方法" 结合、访问限定符控制权限,让代码更安全易维护;
- 重点突破:
- 类的定义与实例化(类是模板,对象是实例);
- 对象大小计算(成员变量 + 内存对齐,空类 1 字节);
- this 指针(隐含传递、特性、面试题);
- 访问限定符与封装(struct 和 class 的区别);
- 学习方法:
- 多敲代码:每个示例(尤其是错误示例)都要编译运行,观察报错信息;
- 对比学习:通过 C 和 C++ 实现 Stack 的差异,体会面向对象优势;
- 聚焦面试:内存对齐、this 指针、struct 与 class 区别是高频考点;
- 避坑清单:
- 类定义结尾不要漏分号;
- 类外定义成员函数必须加
类名::; - 成员变量命名要区分形参(如加
_); - 空类大小为 1 字节,非静态成员函数不占对象空间。