文章目录
const关键字
被它修饰的值不能改变,是只读变量。必须在定义的时候就给它赋初值。
常量指针(底层const:指针所指的对象是常量)
是指定义了⼀个指针,这个指针指向⼀个只读的对象,不能通过常量指针来改变这个对象的值。常量指针强调的是 指针对其所指对象的不可改变性。
具体可看这篇文章:C++语法|C++八股|指针常量和常量指针、函数指针和指针函数
示例
int temp = 10;
const int* a = &temp;
int const *a = &temp;
// 更改: *a = 9; // 错误:该值为只读对象
temp = 9; // 正确
指针常量(顶层const:指针本身是常量)
指针常量是指定义了⼀个指针,这个指针的值只能在定义时初始化,其他地⽅不能改变。指针常量强调的是指针的 不可改变性。也就是引用的本质。
具体可看这篇文章:C++语法|C++八股|指针常量和常量指针、函数指针和指针函数
示例
int temp = 10;
int temp1 = 12;
int* const p = &temp;
// 更改:
//p = &temp1; // 非法
*p = 9; // 正确
const和define的区别
const
⽤于定义常量;⽽define
⽤于定义宏,⽽宏也可以⽤于定义常量。都⽤于常量定义时,它们的区别有:
-
const
⽣效于编译的阶段;define
⽣效于预处理阶段。 -
const
定义的常量,在C语⾔中是存储在内存中、需要额外的内存空间的;define
定义的常量,运⾏时是直接的操作数,并不会存放在内存中。 -
const
定义的常量是带类型的;define
定义的常量不带类型。因此define
定义的常量不利于类型检查。
static关键字
static
关键字主要⽤于控制变量和函数的⽣命周期、作⽤域以及访问权限。
这些都是由于static
将修饰的变量变为了静态变量,放到了全局区,只有程序运行结束才会被释放。
静态函数
静态函数通常指在全局作用域内使用static关键字声明的函数。这类函数的作用域仅限于定义它们的文件内,即它们在自己的编译单元(通常是一个源文件)内是可见的,而在其他编译单元内是不可见的。
这意味着其他源文件中不能链接到这个静态函数。使用静态函数可以减少全局命名空间的污染,避免在不同源文件中定义同名函数的冲突。
// 在 file1.cpp 中
static void utilityFunction() {
// 函数实现
}
// utilityFunction 只能在 file1.cpp 中使用
此案例中就是静态成员函数
静态成员函数
- 在类中使⽤ static 关键字修饰的成员函数是静态成员函数。
- 静态成员函数不能直接访问⾮静态成员变量或⾮静态成员函数。
- 静态成员函数可以通过类名调⽤,⽽不需要创建类的实例。
class ExampleClass {
public:
static void staticFunction() {
cout << "Static function" << endl;
}
};
// 除本文件外还可以通过 MyClass::staticMemberFunction() 调用
静态函数和静态成员函数区别
- 作用域和可见性:静态函数的作用域限于它们所在的源文件,这有助于隐藏实现细节和防止名称冲突。而静态成员函数属于类的一部分,它们的作用域被限定在类内,但可以被任何能够访问该类的代码调用。
- 访问权限:静态成员函数可以访问类的静态成员变量和其他静态成员函数,但不能直接访问类的非静态成员(包括变量和函数)。普通的静态函数则没有这样的限制,因为它们不属于任何类。
- 用途:静态函数通常用于文件内部的辅助函数,希望这些函数不被其他文件直接访问。静态成员函数通常用于执行不依赖于类实例的操作,如工厂方法、单例模式的实例获取等。
静态变量
静态变量在C++中通常指的是使用static关键字声明的全局变量或类的静态成员变量。全局静态变量的作用域限于定义它的文件,
static int globalVar; // 仅在定义它的文件内可见
静态成员变量
- 在类中使⽤
static
关键字修饰的成员变量是静态成员变量。 - 所有类的对象共享同⼀个静态成员变量的副本。
- 意思就是说静态成员变量是属于整个类的,而不是属于类的某个特定对象。
静态成员变量staticVar
是由所有ExampleClass
的实例共享的,这表明在任何对象中对其的修改都会反映在类的所有对象中。
- 意思就是说静态成员变量是属于整个类的,而不是属于类的某个特定对象。
- 静态成员变量必须在类外部单独定义,以便为其分配存储空间。如果实在想在内部定义可以这样写:
static const int num = 1;
class ExampleClass {
public:
static int staticVar;
};
// 静态成员变量声明
// 静态成员变量定义
int ExampleClass::staticVar = 0;
静态局部变量
- 在函数内部使⽤
static
关键字修饰的局部变量是静态局部变量。 - 静态局部变量的⽣命周期延⻓到整个程序的执⾏过程,但只在声明它的函数内可⻅。
void exampleFunction() {
static int localVar = 0;
localVar++;
cout << "LocalVar: " << localVar << endl; }
define和typedef关键字
define
和typedef
在C/C++中都用于定义别名,但它们的用法和目的存在明显的区别
define
define
是预处理指令,用于定义宏。它在编译之前进行文本替换。define
可以用来定义常量值、函数、代码片段等。- 由于
define
进行的是文本替换,所以它不受类型检查的约束。 define
可以定义不仅仅是类型别名,还可以定义方法别名,或者是某个复杂的表达式。
示例
#define PI 3.14159
#define SQR(x) ((x) * (x))
typedef
typedef
用于为现有的数据类型定义一个新的名称(别名)。这是在编译时处理的,不是文本替换。typedef
主要用于简化复杂的数据类型声明,如结构体、联合体和指针类型等。- 使用
typedef
定义的新名字完全遵循原类型的所有属性,这包括类型检查。 typedef
给类型定义别名,使代码更易于理解和维护。
typedef unsigned long ulong;
typedef int (*funcPtr)(int, int);
define和typedef的区别
- 作用范围:
define
是一个宏,作用在预处理阶段,可以用于定义任何文本替换模式;而typedef
仅限于为类型定义新的别名。 - 类型检查:
typedef
遵循严格的类型检查,而define
则不遵循,因为define
仅仅是文本替换。
typedef int* IntPtr;
float f;
IntPtr p = &f; // 这里会产生编译错误,因为IntPtr是指向int的指针,而f是一个float。
#define IntPtr int*
float f;
IntPtr p1 = &f, p2; // p1试图成为float*,但预处理器替换使其看似合法。但实际上,这是不安全的,p2成为了int类型变量。
- 使用场景:
define
可以定义常量、宏函数等,而typedef
主要用于定义类型的别名。
经典案例
#define INTER1 int*
typedef int* INTER2;
INTER1 p1,p2;
INTER2 P3,P4;
其中,p2是整数型变量,p1,p3,p4都是整数指针。
注意,定义指针一定要int *p1, *p2
如果是int *p1, p2
那么p2
是一个整形变量。就跟上文所述的案例一致了。
总的来说,尽管define和typedef都可以用于定义别名,但它们的应用场景和实现方式大相径庭。typedef提供了类型安全的别名定义,而define则提供了更为灵活的文本替换功能。
inline关键字
inline
是先将内联函数编译完成⽣成了函数体直接插⼊被调⽤的地⽅,减少了压栈,跳转和返回的操作。
没有普通函数调⽤时的额外开销。
inline关键字是C++中用于减少函数调用开销的一个工具,它通过建议编译器将函数调用替换为函数体本身来实现。
注意事项
- 编译器的自由:inline仅仅是一个建议,最终是否内联取决于编译器的决策。编译器可能因为各种原因(如函数体过大、包含复杂控制语句、递归调用等)决定不进行内联。
- 定义的可见性:内联函数的定义需要在调用之前或在调用的文件中可见。这通常意味着内联函数的定义放在头文件中。
- 一致性:如果一个函数在多个地方被声明为内联,那么它的所有定义必须完全相同。
- 适用场景:适合内联的函数通常是那些简短且频繁被调用的函数。对于复杂或不常调用的函数使用内联可能适得其反,因为这会增加二进制文件的大小。
C++中inline
编译限制
- 不能存在任何形式的循环语句
- 不能存在过多的条件判断语句
- 函数体不能过于庞⼤
- 内联函数声明必须在调⽤语句之前
define和inline的区别
先论述内联关键字的定义,然后论述define
关键字定义即可:定义预编译时处理的宏,只是简单的字符串替换,⽆类型检查,不安全。
constexpr关键字
constexpr的作用
- 提升性能:
constexpr
允许在编译时进行计算,减少运行时的计算开销。 - 类型安全:使用
constexpr
可以在编译时捕捉更多错误。 - 模板和元编程:
constexpr
在模板编程和元编程中特别有用,允许更复杂的编译时计算。
定义方式
- 变量:当用于变量时,
constexpr
指示该变量的值在编译时是已知的,并且是常量表达式。 - 函数:当用于函数时,
constexpr
指示函数可以在编译时求值,前提是所有参数都是常量表达式,并且函数体符合constexpr
函数的要求。
示例
constexpr
变量
指示该变量的值在编译时是已知的,并且是常量表达式。
constexpr int max_size = 100; // 编译时常量
constexpr double gravity = 9.81; // 编译时常量
这里,max_size
和gravity
在编译时就已经确定了值,可以用于数组大小声明等场合,也可以确保它们的值不会被修改。
constexpr
函数
指示函数可以在编译时求值,前提是所有参数都是常量表达式
constexpr int square(int x) {
return x * x;
}
int main() {
constexpr int sq = square(10); // 编译时计算square(10)
int array[sq]; // 使用constexpr变量作为数组大小
return 0;
}
在这个例子中,square
函数被定义为constexpr
,这意味着如果它的参数是编译时常量,那么它的返回值也可以在编译时计算。这使得sq
成为编译时常量,可以用作数组大小等编译时上下文中。
constexpr和const区别
constexpr
只能定义编译期常量,⽽ const
可以定义编译期常量,也可以定义运⾏期常量。
constexpr
声明提供了比const
更强的约束。constexpr
保证了变量或函数在编辑期就是已知且不变的,因而自动满足const
的要求。但是,仅仅将变量或函数声明为const
并不能保证它们能用于需要编译时确定值的场景。换言之,constexpr
是一种更严格形式的const
。
volatile关键字
当在C或C++代码中使用volatile指令关键字时,是在告诉编译器,这个变量的值可能会意外地改变。这种改变不是通过程序代码的直接操作实现的,而是可能由操作系统、硬件或其他并发执行的线程(在多线程程序中)造成的。因此,编译器在处理这些volatile变量时会采取一种保守的策略,确保每次访问变量时都直接从其内存地址中读取数据,而不是使用可能已经存储在寄存器中的旧值。
volatile的使用
- 声明语法:
volatile int counter;
- 用途示例:在嵌入式系统中读取硬件寄存器的值,或者在使用中断服务程序(ISR)修改的变量等场景下,这些变量的值可能在任何时候被外部事件改变。
- 读取硬件寄存器的值:在嵌入式系统中,硬件设备的状态通常通过读取特定的内存地址(硬件寄存器)来获取。这些地址对应的值可能会因为硬件事件(如按键被按下、传感器值改变等)而改变,而这种改变是独立于程序控制的。
- 使用中断服务程序(ISR)修改的变量:在使用中断服务程序时,程序的主循环可能在任何时刻被中断,以响应外部事件。ISR中可能会修改一些变量,而这些变量同样在主循环中被访问。
示例:
假设我们有一个计时器中断,每当计时器溢出时,就会增加一个tickCount变量的值。同时,主程序循环会检查这个变量的值以执行某些任务。
在这个例子中,tickCount
是一个volatile
变量,因为它在中断服务程序中被修改,而在主循环中被访问。声明为volatile
确保了每次访问tickCount
时,都会直接从内存中读取其最新值,即使编译器可能认为没有必要这么做(基于它的本地优化策略)。
volatile unsigned int tickCount = 0;
// 中断服务程序
void Timer_ISR() {
tickCount++; // 中断更新tickCount
}
int main() {
while (true) {
if (tickCount >= 1000) {
// 执行某些操作
tickCount = 0;
}
}
}
在这个案例中,如果不使用volatile关键字修饰,可能会导致以下情况发生:
-
缓存变量值
编译器可能会假设主程序循环中的tickCount
变量值在没有明显的修改指令的情况下不会改变,因此可能会将其值缓存在寄存器中。这意味着即使ISR更新了内存中的tickCount
的值,主循环中的代码还是使用的寄存器中的旧值。这将导致主程序不能正确响应由ISR更新的tickCount
值。 -
省略读取操作
如果编译器决定将tickCount
的值缓存在寄存器中,那么在循环的每次迭代中,它可能不会从内存中重新读取tickCount
的值。这样,即使tickCount的实际值已经被ISR改变,这个变化也不会反映到主循环中使用的值上。 -
优化掉看似“无用”的读取
编译器的另一个可能的优化是完全优化掉对tickCount
的读取操作,特别是在它认为这个变量值未被修改的情况下。这可能导致程序逻辑上完全忽略了ISR对变量的更新。
volatile的作用
- 防止编译器优化:编译器为了提高性能,可能会进行一些优化操作,比如将变量缓存在寄存器中,或者省略看似多余的读写操作。使用volatile可以禁止这些优化,确保每次访问都直接读取内存中的最新值。
- 增加程序的可移植性:在不同的编译器和优化级别下,保证对易变变量的访问都是一致的。
- 确保多线程程序的数据同步:在某些情况下,可以使用volatile来确保在多线程环境下共享变量的访问不会被编译器优化掉。但是,需要注意的是,volatile并不能替代线程同步机制(如互斥锁),它不保证原子性和内存可见性问题。
extern关键字
extern
关键字在C和C++中用于声明一个变量或函数是在别的文件中定义的,目的是为了在当前文件中能够访问到它。使用extern可以在多个文件之间共享全局变量或函数。
用途和作用
- 共享全局变量:当你有一个全局变量在一个文件中定义,并希望它能在其他文件中被访问和修改时,可以在其他文件中使用
extern
关键字来声明这个全局变量。这样,多个文件就可以共享同一个全局变量的实例。 - 共享函数声明:对于函数,
extern
可以用来在一个文件中声明另一个文件中定义的函数。这实际上是默认行为,所以在函数声明前使用extern
关键字是可选的。不过,在某些情况下显式声明可以增加代码的可读性。
示例
我们有两个源文件:main.cpp
和helper.cpp
。
helper.cpp
// 定义一个全局变量
int globalVar = 42;
// 定义一个函数
void helperFunction() {
// 函数实现
}
main.cpp
#include <iostream>
// 使用extern声明helper.cpp中定义的全局变量和函数
extern int globalVar;
extern void helperFunction();
int main() {
std::cout << "Global variable: " << globalVar << std::endl;
helperFunction();
return 0;
}
在这个例子中,main.cpp
通过extern
关键字访问了helper.cpp
中定义的全局变量globalVar
和函数helperFunction
。
注意事项
- 变量初始化:使用
extern
声明变量时不能进行初始化。初始化应该在变量的定义处完成。 - 链接性:
extern
声明的变量和函数具有外部链接性(external linkage),意味着它们在整个程序中是可见的,除非它们的名称在局部作用域中被重新定义。 - 静态全局变量:如果全局变量前使用了
static
关键字,那么这个变量只在定义它的文件中可见,即使使用了extern
也无法访问它。
std::atomic关键字
用两个问题引入,方便理解。
a++ 和 int a = b 在C++中是否是线程安全的?
案例一、a++ 的线程安全问题
1.a++的线程安全问题
操作a++实际上包含三个步骤:读取a的值、增加a的值、将新值写回a。这个过程被称为“读-改-写”操作。如果两个或多个线程同时执行a++,它们可能会“看到”相同的原始值,因此就会有多个线程计算出相同的增加后的值并试图写回这个值。这会导致竞态条件(race condition),其中只有一个线程的增加操作会被保留,其他线程的操作就会丢失,因此a的最终值会比预期小。
线程安全性的提高
为了确保这类操作的线程安全性,可以采用以下策略之一:
- 互斥锁(Mutex):在执行操作前获取锁,在完成后释放锁。这确保了在任何时刻只有一个线程能执行这个操作。
- 原子操作:使用C++11引入的原子类型std::atomic来保证操作的原子性。例如,使用std::atomic代替int,并使用其提供的fetch_add方法来代替a++操作,以确保操作的原子性。
示例
#include <atomic>
std::atomic<int> a(0);
void safeIncrement() {
a.fetch_add(1); // 原子操作,线程安全
}
我们定义了一个std::atomic<int>
类型的全局变量a
,并初始化为0
。我们的目标是在多个线程中安全地对它进行自增操作。safeIncrement
函数通过调用fetch_add(1)
来安全地自增a
,保证了即使多个线程同时尝试增加a
的值,每次操作也是原子的,从而避免了数据竞争和竞态条件。
案例二、 int a = b的线程安全问题
2.int a = b的线程安全问题
就int a = b
操作本身而言,如果b
的值不被其他线程修改,这个操作在大多数情况下是线程安全的,因为它只是读取b
的值并将其赋给a
。然而,如果b
可以被多个线程访问和修改,那么在不同线程读取b
值的同时另一个线程可能正在修改b
的值,这就造成了数据的不一致性。
示例代码
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<int> a(0);
std::atomic<int> b(0);
void copyValue() {
int localCopy = b.load(); // 从b安全地读取值
a.store(localCopy); // 安全地将值存储到a
}
int main() {
b.store(42); // 假设我们在某处设置了b的值
std::thread t(copyValue); // 在另一个线程中复制值
t.join(); // 等待线程完成
std::cout << "Value of a after copy: " << a.load() << std::endl;
return 0;
}
a
和b
都被声明为std::atomic<int>
类型,这意味着对它们的所有操作都是原子的,因此是线程安全的。copyValue
函数首先使用load
方法从b
安全地读取其值到一个局部变量localCopy
中,然后使用store
方法将这个值安全地存储到a
中。这两个步骤确保了即使在并发环境下,int a = b;
的操作也是线程安全的。- 在
main
函数中,我们修改了b
的值,然后创建了一个线程来执行opyValue
函数,该函数将b
的值复制到a
。我们通过join
等待线程完成操作,然后打印出a
的值。