Day0 基础概念

C++工作原理:

预处理:

处理文件内的预处理语句,如#include、宏定义、条件编译(#if #endif)等。

把文本处理成实际的样子

主要任务:

  • 宏替换:替换 #define 定义的宏。
  • 头文件展开:将 #include 的头文件插入到代码中。
  • 条件编译:根据 #ifdef 等条件指令包含或忽略某些代码。
  • 删除注释:移除所有 ///* ... */ 的注释内容。

输出:

一个“无注释、无预处理指令”的纯C++代码文件,通常以 .i.ii 为后缀。

编译

编译就是把处理后的代码文本,翻译成汇编的过程。

编译器会对源代码进行词法分析、语法分析、语义分析和中间代码生成(生成汇编)。

主要任务:

  • **语法分析:**检查代码的语法是否正确
  • **语义分析:**检查变量类型、函数调用等语义是否正确。
  • **优化:**编译器对代码进行初步优化,移除无用代码、简单循环展开、常量折叠等
  • **生成中间代码:**翻译特定架构的中间代码。

输出:

一个 .s 汇编文件,包含汇编指令。

汇编

汇编就是将汇编代码翻译成二进制机器码。

这一阶段由汇编器完成

主要任务:

  • 将汇编代码翻译成,目标机器指令
  • 生成目标文件,通常是二进制格式的 .o.obj 文件。

输出:

目标文件(.obj文件),包含机器码,但还不是一个可执行程序。

连接

链接阶段,将多个目标文件(.obj)及库文件合并,生成最终可执行文件(.exe)。

主要任务:

  • **符号解析:**将函数或变量的声明和定义匹配(声明只是在编译期间保证通过语法检测,需要在连接阶段找到真正的函数定义)
  • 库的连接:链接静态库.a/.lib)或 动态库.so/.dll)。
  • 地址分配:为每个符号分配内存地址
  • 合并代码段和数据段:将所有模块的代码和数据整合为一个整体。

输出:

一个最终的可执行文件,通常以 .exe 或无扩展名的形式存在。

完整的编译流程

  • 预处理:将 file.cpp 转换为 file.i
  • 编译:将 file.i 转换为 file.s
  • 汇编:将 file.s 转换为 file.objj。
  • 链接:将 file.obj 转换为 file.exe

工具与阶段对应关系

阶段 输入文件 输出文件 工具
预处理 .cpp.cc .i 预处理器 (g++ -E)
编译 .i .s 编译器 (g++ -S)
汇编 .s .o 汇编器 (g++ -c)
链接 .o 可执行文件 链接器 (ld/g++)

“->”和“.和“::”的区别

**-> **是指针指向其成员的 运算符

->前面放的是指针

指针->成员方法

1
2
3
4
5
6
7
8
9
10
class Printable
{
public:
virtual std::string GetClassName() = 0;
};

void PrintClassName(Printable* obj) // Printable类型 的指针
{
std::cout << obj->GetClassName() << std::endl; // 调用这个指针的成员函数
}

**:: **是域作用符

::前面放的是域性质的实体(类、命名空间等)

**::**是域作用符,是各种域性质的实体(比如类(不是对象)、名字空间等)调用其成员专用的。
(如果有个局部变量与全局变量同名(假设都是int a;),默认调用的 a 是局部变量,如果要访问全局变量a,就要这么写“::a”。使用域作用符来加以区别;前面没写具体的域名,就是指默认域)

例如调用类的静态函数or静态变量

类型::静态变量/静态函数

. 是成员作用符

. 是对象专用的,只有类实例化的对象才使用.

Day1 基本语法

注释

单行:

1
// 注释内容

多行:

1
2
3
/* 
注释内容
*/

和Python完全不一样啊属于是

Python

单行:

1
# 注释内容

多行:

1
2
3
"""
注释内容
"""

其中C++还有个比较特别的东西

条件编译

1
2
3
#if bool
code
#endif

当bool为真时,执行code,否则不执行。

可以在测试环境时让bool为真,在发布环境时让bool为假,从而实现不同环境下的代码执行。

输出输入

在C中,还是和Python用的差不多的,是用printf()

不过到了C++,有全新的东西:cout

cout作为C++的特色,在写C++语言时都偏向使用cout来输出文本到控制台。

使用cout而不使用printf的原因

  • 类型安全
  • 面向对象设计
  • 易于扩展

类型安全

std::cout:通过 C++ 的 流插入操作符 (<<),可以避免手动指定数据类型。编译器会根据数据的类型自动选择正确的处理方式。

1
2
int num = 42;
std::cout << "The number is: " << num << std::endl;

这里 num 的类型自动匹配,不需要手动指定格式。

printf:需要手动指定格式(如 %d, %s 等),如果格式与变量类型不匹配,会导致未定义行为:

1
2
int num = 42;
printf("The number is: %f\n", num); // 未定义行为,类型不匹配

面向对象设计

std::cout:是 C++ 标准库中的一个对象,体现了面向对象的设计原则,更加符合 C++ 的设计理念。

printf:是 C 的标准库函数基于函数调用,缺少对象的灵活性。

易于扩展

使用 std::cout 可以通过重载 operator<< 为用户自定义类型实现输出:

这个后面学到类的时候再细看

<< endl的作用

  • 换行符:会插入换行符 类似\n。
  • 刷新缓冲区:会强制刷新输出缓冲区。确保之前的数据立即显示到终端或写入文件。普通的\n换行符并不一定会刷新缓冲区。

联合体

联合体是C++中一种数据结构,类似结构体struct

(在我看来 struct就是一个存放变量的类而已)

但是联合体有个很牛逼的特性:

  • 就是联合体里面的成员,共享一块内存

除此之外,还有其他相关的特性:

  • 它的所有成员相对于内存地址的偏移量都为0;即所有成员的内存地址都是从整个联合体的头部开始的。

  • 联合体的内存空间,需要大到足够容纳”最宽“的成员;即联合体的内存占用大小取决于,占用内存最大的成员的内存大小。

img

Day2 递归、结构体、枚举、静态变量等

枚举

  • 关键字 enum,代表声明的是一个枚举类型的数据结构

  • 定义格式

1
enum <类型名> {<枚举常量表>};
  • enum :表明后面的标识符是一个枚举类型的名字

  • 枚举常量表:以标识符的形式表示的整型量

  • 枚举常量、或者叫枚举成员,只能是整型常量。

成员为什么只能是整型常量?

  1. 历史原因: 枚举在 C 和 C++ 中的起源是替代 #define 宏定义,提供一组有意义的整型常量,因此设计成整型。
  2. 性能和简洁性: 整型常量在底层实现中直接映射为数值,访问和比较高效。
  3. 用途特点: 枚举通常用来表示状态或选项,整型数值便于操作和存储。

其实就是宏的一种,作用也是和宏一样:定义一组有意义的整型常量。

在用的时候,需要定义一个枚举类型的变量,才能开始使用

  • 使用格式:

    1
    枚举类型名称 枚举变量名称 = 枚举常量

要注意:

  • 枚举常量的标识符不能相同,否则会造成命名冲突。(可以理解为不能有两个名字相同的宏定义就行)

静态变量

  • 关键字 static,声明一个变量为静态变量
  • static是一种 类型限定符
  • 用于定义静态变量,表示该变量的作用域仅限于当前文件当前函数内,不会被其他文件或函数访问。

随机数

Day3

指针

定义:指针是一个整数,一种存储内存地址的数字

指针只是指向内存中的一个位置,是一个整数 变量

基础用法:

1
2
3
4
5
6
7
8
int main()
{
int var = 8;
int* ptr = &var; # <type>* 定义一个type类型的 指针。(这里的类型没有意义,只是告诉编辑器应该如何读写这个指针变量。) &:获取这个对象的内存地址
*ptr = 10; # 通过* 访问指针指向的内存地址
LOG(var);
std::cin.get();
}

指针:只是一个保存内存地址的整数

1
2
3
4
5
** 双指针,指向指针的指针(一个内存地址变量的内存地址,因为指针也是一个变量,也需要一个内存地址来存放)

char** ptr = &buffer; // 指针的指针 ptr是一个指针,这个指针是 存放&buffer地址 的内存地址
// 现在的ptr,保存的是&buffer指针的内存地址,渠道

引用

引用是指针的语法糖,让指针更容易阅读、理解

一种引用现有变量的方法

必须引用已有的变量

例:

1
2
int& a = b;		//	初始化一个整形对象的引用 a,引用b
// 这时,a就可以看作是b的别名,修改a的值也会修改b的值

引用能做的,指针都可以完成,引用只是指针的语法糖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void Incremenet(int*  num)//传递指针,通过逆引用修改指针指向的对象的值
{
(*num)++;
}

void Incremenet2(int& num)//通过引用,这里的num等于参数的引用,修改num的值也会修改实参的值
{
num++;
}


int main()
{
int a = 5;
Incremenet(&a);
Incremenet2(a);
//int& ref = a;


LOG(a); // Output: 2
std::cin.get();
}

类和结构体的区别

类:

1
2
3
4
5
6
7
8
9
10
11
12
class Player
{
public:
int x, y;
int speed;

void Move(int dx, int dy)
{
x += dx * speed;
y += dy * speed;
}
};

类中,参数的访问性默认是private的,只允许类内部访问。

而struct,默认是public的

技术上来讲,就只有参数的默认访问性不一样

结构体存在的意义是,为了向C向下兼容,因为C中并没有class,但是有struct

选择用那种,基本上是看编码习惯

例如struct,通常是定义一堆变量的集合,没有行为

class 则是定义 变量和行为的集合体,更

Day4

静态 static

有两种含义

类or结构体 外部使用static

代表这个 变量 或者 方法 只在当前翻译单元内部链接(在声明的文件内有效)

在链接阶段,不会在当前翻译单元外连接定义和声明

使用说明

1
2
3
4
5
static int s_Variable = 5;

static void Function()
{
}

如果在别的文件,想要使用这个静态变量,则需要通过extend

1
extern int s_Variable;

类or结构体 内部使用static

静态 变量会在所有类实例中共用内存

静态方法可以被调用,不需要通过类实例,而在静态方法内部

静态变量or方法的调用,通过::调用

1
2
3
4
5
6
7
8
9
10
struct Entity
{
int x, y;

static void Print()
{
std::cout << "x: " << x << " y: " << y << std::endl;
}
};

且静态函数,不能使用非静态变量,因为静态函数不能引用到类的实例。

甚至可以看作是一个写在类外部的函数,只是因为分类or职能相同,所以放在了类里

意味着该变量实际上将与类的所有 实例 共享内存

局部静态Local Static

局部静态变量,允许我们声明一个变量,该变量的

生命周期:等于整个程序的生命周期

作用域:被限制在定义它的作用域内。

例如如果是在一个函数内定义,则只有这个函数能直接访问到这个局部静态变量,但是这个变量在离开作用域后不会被销毁,而是继续存活着

下次再调用这个函数,不会重新创建这个变量,而是返回已有的

最经典的用法就是单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Singleton
{
public:
// 返回Singleton的单例
static Singleton& Get()
{
// 在函数第一次被调用的时候,初始化一个Singleton的实例,instance,并且返回这个实例的引用
// 在之后的调用中,直接返回这个实例的引用
// 这样做的好处是,保证了Singleton的单例
static Singleton instance;
return instance;
}

void Print()
{
std::cout << "Singleton" << std::endl;
}
};

通过Get获取的实例,都是同一个,可以通过Get来获取类的单例。

这里返回需要返回对象的引用,如果没有&符号,会返回这个对象的复制对象,就不是单例了。同理static Singleton instance的作用就是在第一次调用的时候实例化Singleton类,并且之后的调用中直接返回这个实例,不会再创建。

枚举

枚举:是整数,将一组数值集合作为类型,而不是仅仅用整型作为类型

一堆同类的数值,为了方便阅读代码,把一些有意义的整数枚举出来。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class Log
{
public:
enum Level
{
LevelError = 0,
LevelWarning = 1,
LevelInfo = 2
};

private:
Level m_LogLevel = LevelInfo;

public:
void SetLevel(Level level) // 定义了level是Level枚举类的实例,就意味着level只能是枚举里面的枚举常量,如果不是,编译器会报错
{
m_LogLevel = level;
}

void Info(const char* message)
{
if (m_LogLevel >= LevelInfo) // 类里可以直接调用枚举里面的枚举常量
std::cout << "[INFO]:" << message << std::endl;
}



int main()
{
Log log;
log.SetLevel(Log::LevelError); //外部要调用的话,要通过类的命名空间调用,struct自己没有命名空间
}

构造函数

可以理解为Python中的 __init__,负责对象的初始化

在类实例化对象时会自动调用。

而C++中的构造函数,是命名和类一样的函数。

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class Entity
{
public:
float m_x, m_y;

Entity() // 构造函数,在创建对象时自动调用
{
m_x = 0.0f;
m_y = 0.0f;
}

Entity(float x, float y) // 重载构造函数,重载:同名的函数,但参数不同,在类实例化对象时,可以通过是否输出参数来自动选择构造函数
{
m_x = x;
m_y = y;
}

void Print()
{
std::cout << "x: " << m_x << " y: " << m_y << std::endl;
}
};


int main()
{
Entity e1;
e1.Print();

Entity e2(10.0f, 2.0f);
e2.Print();

std::cin.get();
return 0;
}

而我们可以通过把构造函数私有化,不允许外部调用构造函数,来实现单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class Log
{
private:
Log() {} // 构造函数为私有,禁止创建对象

public:

static Log& Get() // 外放获取单例对象的函数
{
static Log log; // 静态局部对象,只创建一次
return log;
}

static void Write(const char* message)
{
std::cout << message << std::endl;
}

};


int main()
{
Log::Write("Hello, world!"); // 现在只可以通过调用静态方法

Log& log = Log::Get();
log.Write("Hello, world!"); // 或者 通过接口获取单例对象,调用实例方法。
// 不能通过 Log log;来实例化对象,因为对象的构造函数被设置为私有了
std::cin.get();
return 0;
}

析构函数

假如手动在 堆分配了内存,需要手动在对象销毁的时候手动释放内存,否则会造成内存泄漏。

通常不会手动调用。析构函数就是构造函数前面多加个~

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

class Entity
{
public:
float m_x, m_y;

Entity() // 构造函数,在创建对象时自动调用
{
m_x = 0.0f;
m_y = 0.0f;
std::cout << "Entity created" << std::endl;
}


~Entity() // 析构函数,在对象销毁时自动调用
{
std::cout << "Entity destroyed" << std::endl;
}

Day5

继承

继承没什么好说的,面向对象的基础

不过c++里面,继承还有访问修饰符,public、private这些

语法:

class 类名 : 修饰符 父类名

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Entity		// 实体类
{
public:
float X, Y;

void Move(float xa, float ya)
{
X += xa;
Y += ya;
}
};

class Player : public Entity // 玩家类继承实体类,public继承
{
public:

const char* Name;

void PrintName()
{
std::cout << "Player Name: " << Name << std::endl;
}
};

虚函数

定义

虚函数是为了允许在子类中,重写父类的方法。(函数名相同,参数也相同)

相关知识点:

虚函数表(Virtual Table)简称vtbl

通过关键字 virtual 声明函数为虚函数,只有声明了虚函数的函数才能被子类重写,否则调用类的方法会调用到父类的方法。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Entity			//父类
{
public:
float X, Y;

void Move(float xa, float ya)
{
X += xa;
Y += ya;
}

virtual std::string GetName() // 允许被子类重写的函数,必须声明为虚函数
{
return "Entity";
}
};


class Player : public Entity
{
private:
std::string m_Name;
public:

void PrintName()
{
std::cout << "Player Name: " << m_Name << std::endl;
}

Player(const std::string& name) : m_Name(name)
{
}

std::string GetName() override // 重写父类的函数,需要声明override,方便编译器提示
{
return m_Name;
}
};

大概说明一下虚函数的实现原理:这里应该也是面试会问到的问题,就按照面试的答案来吧。

实现原理:

虚函数表(vtable)

  • 当类拥有>=一个虚函数时,就会拥有一个自己的虚函数表。它是一个指针数组,每个元素都是函数指针。
  • 如果派生类覆盖了基类的虚函数,虚函数表中的指针,就会被替换成派生类实现的函数的函数指针。

虚指针(vptr)

  • 每个拥有虚函数的类,都会隐式地包含一个虚指针(vptr,在编译期间编译器机制会自动补充虚指针的初始化),这个虚指针指向当前对象所属的虚函数表。
  • 当通过对象调用虚函数时,编译器通过虚指针找到虚函数表,再通过函数的偏移量找到对应的函数指针,再找到对应的函数。

调用流程

  • 通过基类指针或引用调用虚函数时:
    1. 通过对象的虚指针找到虚表
    2. 从虚表中查找对应的函数指针
    3. 调用函数指针指向的函数

img

子类如何替换虚函数

子类继承父类时,虚表的构造方式如下:

  1. 继承虚表
    • 子类的虚表开始时会拷贝父类的虚表。
  2. 覆盖虚函数
    • 如果子类重写了某个虚函数,虚表中对应的位置会被替换为子类实现的函数指针。
  3. 新增虚函数
    • 如果子类新增了虚函数,虚表中会扩展新的条目。

动态绑定与静态绑定

  • 静态绑定

    (非虚函数):

    • 函数调用在编译时解析,直接生成具体的函数地址。
    • 比虚函数调用效率更高。
  • 动态绑定

    虚函数):

    • 函数调用在运行时解析,通过虚表实现
    • 需要额外的指针查找和表查询,因此略有性能开销。

常见问题:

(1) 为什么构造函数中不能调用虚函数?

  • 在构造函数执行期间,虚指针可能尚未初始化或仍指向基类的虚表。
  • 因此,调用虚函数时不会有动态绑定,而是直接调用基类版本。
  • 虚函数表的创建需要用到类对象,但是在调用构造函数前,类对象还没被生成,所以构造函数无法是虚函数

(2) 为什么析构函数应该是虚函数?

  • 如果基类指针指向派生类对象,而基类析构函数非虚,则只调用基类的析构函数,导致派生类的资源未释放。
  • 使用虚析构函数确保派生类的析构函数被正确调用

总结

  • 如果基类希望函数可以、应该被子类重写,那这个基类的函数就应该声明为虚函数
  • 虚函数通过虚指针和虚表实现
    • 虚指针编译期间自动生成
    • 虚表是一个函数指针数组,每个类有自己的虚表
  • 拥有虚函数的类,对象的内存布局中,通常第一条是vptr指针

纯虚函数(接口)

虚函数其实就是接口,在接口类写的纯虚函数,没有函数体,子类必须要实现纯虚函数的实现。

纯虚函数允许我们在基类中定义一个没有实现的函数,然后强制子类去实现该函数。

例子

1
2
3
4
5
6
class Printable
{
public:
virtual std::string GetClassName() = 0; // 纯虚函数的定义 没有函数体, 虚函数 返回类型 函数名 = 0;
};

就是接口

可见性

可见性修饰符

  • private
    • 完全隐藏:仅当前类可见,甚至子类都不能访问
    • 使用场景:
      • 用于封装类的内部实现细节,不希望外界或子类直接操作这些成员。
  • protected
    • 对外不可见继承时可访问:仅当前类以及子类可见
    • 使用场景:
      • 保护类的内部状态,使得只有子类能够操作或访问这些数据。
  • public
    • 完全公开:类的成员在类外部可以直接访问。
    • 使用场景:
      • 对外提供接口(如 getter/setter 函数、构造函数等)。
      • 让外部直接访问不需要隐藏的信息(变量、函数等)。

Day6

数组

定义:

在一个变量中有多个变量,这个变量称作数组。

它可以存储一个固定大小相同类型元素的顺序集合。数组是用来存储一系列数据,但它往往被认为是一系列相同类型的变量。

内存布局

数组在内存中以连续的形式存储

1
2
3
4
5
6
7
int	main()
{
int example[5]; //声明数组,里面有5个元素,每个元素的类型为int 数组是连续的数据块

for (int i = 0; i < 5; i++) // 循环赋值
example[i] = 2;
}

内存视图

image-20241209170325176

由于是int型数组,int型占4个字节,这里5个元素,所以在内存上是20个连着的字节

声明数组

1
type arrayName [ arraySize ];
  • type 可以是任意有效的 C++ 数据类型

  • arrayName是数组名

  • arraySize 必须是一个大于零的 整数常量

例:

1
double balance[10];

现在 balance 是一个可用的数组,可以容纳 10 个类型为 double 的数字。

初始化数组

在 C++ 中,您可以逐个初始化数组,也可以使用一个初始化语句,如下所示:

1
double balance[5] = {1000.0, 2.0, 3.4, 7.0, 50.0};

数组内所有元素都赋值为50.0

1
balance[4] = 50.0;

访问数组元素

这个也是依赖下标

1
double salary = balance[9];

注意下标不要超出边界,在原始数组内,并不会做边界检测,假如操作了数组以外的内存,有可能会改变现有变量的值,导致依赖之外的异常。

静态数组

  • 静态是在栈里创建的,在离开当前作用域后 {} 花括号后就会销毁

  • 定义时就已经在 上分配了空间大小,在运行时这个大小不能改变

1
int example[5];

动态数组

  • 运行时在 上分配一定的存储空间,另外在运行时还可以改变其大小。
  • 虽然在栈上分配空间效率较高,但是栈空间有限,对于大型数据应使用new和delete在堆区分配空间
1
int* another = new int[5];

由于动态数组的生命周期是直到程序结束,所以需要手动调用释放数组占用的内存,否则会造成内存溢出

1
delete[] another;			// 释放动态分配的内存

字符串

定义

字符串, 实际上是是 字符 数组 ,是一堆字符的集合。在c++标准库中,一个char占用一个字节(因为主要是英文,一个16进制的字节足够用了)

image-20241209175159276

主要是学习一下字符串的一个工作原理、

本质上是char字符的数组,就像上面编辑器显示的,双引号 “” 会隐式地转换成字符数组

内存布局

由于字符串本质上是数组,所以也是在内存中以连续的形式存储

image-20241209181936339

由于char占用1个字节,所以 “John” 由4个字节组成,注意末尾由00作为字符串结束符,

1
2
const char* name = "John";		// 会自动添加'\0'
const char* name = "John\0"; // 和这一行的输出效果是一样的

由于本质上是数组实现的,所以也可以这样输出字符串

1
2
char name2[7] = {'M', 'u', 'k', 'P', 'u', 'n', '\0'};		// 必须手动添加 字符串结束符,否则会输出后面的乱码
char name2[6] = {'M', 'u', 'k', 'P', 'u', 'n',}; // 就会出现下面的乱码情况

image-20241209182556932

这种直接操作 char[] or char* 是C语言的风格

c++中是建议使用 std::string

通过指针、字符串数组等形式定义字符串,是C语言的风格,C++为了向下兼容,所以前期也沿用的这种风格

1
const char* str = "Hello, world!";

但是C++11之后,退出了 std::string

初始化字符串变量

1
2
3
4
5

std::string str1 = "Hello"; // 初始化
std::string str2("World"); // 直接构造
std::string str3(5, 'A'); // 5 个 'A' 构成字符串 "AAAAA"
std::string str4 = str1 + " " + str2; // 拼接字符串 "Hello World"

std::string 常见操作

操作 示例代码 说明
获取长度 str.size()str.length() 获取字符串长度
拼接字符串 str1 + str2str1.append(str2) 拼接两个字符串
访问字符 str[0]str.at(0) 访问字符串中某个字符
子串 str.substr(pos, len) pos 开始,长度为 len 的子串
查找子串 str.find("sub") 返回子串首次出现的位置(找不到返回 npos
替换子串 str.replace(pos, len, "new") 替换从 pos 开始,长度为 len 的部分
插入字符串 str.insert(pos, "inserted") pos 位置插入字符串
删除字符或子串 str.erase(pos, len)str.pop_back() 删除从 pos 开始,长度为 len 的部分
比较字符串 str1.compare(str2) 返回 0(相等),正值(大于),负值(小于)
转为 C 风格字符串 str.c_str() 返回指向内部 C 风格字符串的指针

Day7

常量 Const

定义:用于表示固定不变的数据或限制修改权限

1. 表示不可修改的数据(定义常量)

这是常量的最基本用途,即用于声明固定值,确保程序运行中这些值不会被意外修改。

使用场景

  • 表示程序中的固定值,比如物理常量、配置参数、默认值等。
  • 替代宏定义(#define),具有类型检查能力,增强安全性。

例子:

1
2
3
4
void Const()
{
const int MAX_SIZE = 90; // 声明一个常量
}
  • 优点:编译器会强制保证 MAX_SIZE 的值不可修改。
  • 好处:相比宏,const 常量有明确的类型,可以被调试器识别。

这里要明确的说明一下常量指针和指针常量的定义和区别

常量指针

  • 指向常量的指针
  • int const* a = new int; const* 前面,叫做常量指针
  • 指针指向的内容不可改

例子

1
2
3
4
5
6
// 指向常量的指针			这个常量不可改变
int const* a = new int; // 常量指针 const int* 指针指向的内容不允许改变

// 常量指针不允许修改指针 指向的内容
*a = 2; // 修改 指针指向的内容,即是 该内存地址上保存的内容(常量指针这样是不允许的)
a = (int*)&MAX_SIZE; // 允许修改指针本身内容

指针常量

  • 指针本身是常量
  • int* const b = new int;*const前面。叫做指针常量
  • 指针本身内容不可修改

例子:

1
2
3
4
5
6
// 指针本身是常量,		指针本身不可修改
int* const b = new int; // 指针常量 ,指针本身不允许改变

// 指针常量 不允许修改指针本身的值
*b = 4; // 允许修改指针指向的内容
b = (int*)&MAX_SIZE;

指向常量的指针常量:

1
const int* const a = new int;		// 这个指针指向的内容,以及这个指针本身不可改

2. 修饰函数参数(只读参数)

当函数的参数加上 const 修饰后,表示在函数内部不能修改该参数的值。这种设计增强了代码的安全性和可读性,尤其是在以下两种场景中:

1
2
3
4
void printValue(const int value) {
// value = 10; // 错误:无法修改 const 参数
std::cout << "Value: " << value << std::endl;
}
2.2 修饰指针或引用参数

当函数通过指针或引用传递参数时,const 可以防止函数修改传递的对象。

  • 指针示例:
1
2
3
4
void display(const char* str) {
// str[0] = 'H'; // 错误:无法修改指向的内容
std::cout << str << std::endl;
}
  • 引用示例:
1
2
3
4
5
6
7
void printVector(const std::vector<int>& vec) {
// vec.push_back(10); // 错误:无法修改 vec
for (const auto& val : vec) {
std::cout << val << " ";
}
std::cout << std::endl;
}

优点

  • 明确参数只读属性,避免误修改。
  • 提高函数的通用性,尤其在处理大对象时。

3. 修饰成员函数(保证对象状态不变)

在类中,const 可修饰成员函数,表示该函数不会修改对象的状态。这种设计可以提升代码的健壮性和安全性。

使用场景
  • 设计访问器(getter)或只读方法。
  • 确保特定操作不改变对象的成员变量。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Entity
{
private:
int* m_x, *m_y; // 同一行中如果要让全部变量都是指针,需要在变量前加*,否则m_y只是一个普通的int型

public:

const int* const GetX() const // 一行三个const,返回一个不能修改指向且不能修改内容的指针,且这个函数不能修改类里面的属性
{
return m_x;
};

int* GetY() const // 承诺这个函数不会改变类的成员变量
{
return m_y;
};

};

底层机制

const 成员函数内部,this 指针类型为 const ClassName*,因此不能通过 this 修改成员变量。

mutable

如果某些成员变量需要被修改,可使用 mutable 关键字:

1
2
3
4
5
6
7
8
9
10
class MyClass {
private:
mutable int cache; // 可变成员变量

public:
void updateCache() const {
cache++; // 允许修改
}
};

总结:

用途 作用 示例
定义常量 表示不可修改的固定值 const double PI = 3.14;
修饰函数参数 防止参数在函数内部被修改,增强安全性 void display(const std::string& str);
修饰成员函数 保证成员函数不会修改对象状态,支持常量对象调用 int getValue() const;