C++语法|C++八股|const、static、define、inline、constexpr、volatile、extern、std::atomic

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⽤于定义宏,⽽宏也可以⽤于定义常量。都⽤于常量定义时,它们的区别有:

  1. const⽣效于编译的阶段;define⽣效于预处理阶段。

  2. const定义的常量,在C语⾔中是存储在内存中、需要额外的内存空间的;define定义的常量,运⾏时是直接的操作数,并不会存放在内存中。

  3. 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关键字

definetypedef在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编译限制

  1. 不能存在任何形式的循环语句
  2. 不能存在过多的条件判断语句
  3. 函数体不能过于庞⼤
  4. 内联函数声明必须在调⽤语句之前

define和inline的区别

先论述内联关键字的定义,然后论述define关键字定义即可:定义预编译时处理的宏,只是简单的字符串替换,⽆类型检查,不安全。

constexpr关键字

constexpr的作用

  • 提升性能:constexpr允许在编译时进行计算,减少运行时的计算开销。
  • 类型安全:使用constexpr可以在编译时捕捉更多错误。
  • 模板和元编程:constexpr在模板编程和元编程中特别有用,允许更复杂的编译时计算。

定义方式

  • 变量:当用于变量时,constexpr指示该变量的值在编译时是已知的,并且是常量表达式。
  • 函数:当用于函数时,constexpr指示函数可以在编译时求值,前提是所有参数都是常量表达式,并且函数体符合constexpr函数的要求。

示例

constexpr变量

指示该变量的值在编译时是已知的,并且是常量表达式。

constexpr int max_size = 100; // 编译时常量
constexpr double gravity = 9.81; // 编译时常量

这里,max_sizegravity在编译时就已经确定了值,可以用于数组大小声明等场合,也可以确保它们的值不会被修改。

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关键字修饰,可能会导致以下情况发生:

  1. 缓存变量值
    编译器可能会假设主程序循环中的tickCount变量值在没有明显的修改指令的情况下不会改变,因此可能会将其值缓存在寄存器中。这意味着即使ISR更新了内存中的tickCount的值,主循环中的代码还是使用的寄存器中的旧值。这将导致主程序不能正确响应由ISR更新的tickCount值。

  2. 省略读取操作
    如果编译器决定将tickCount的值缓存在寄存器中,那么在循环的每次迭代中,它可能不会从内存中重新读取tickCount的值。这样,即使tickCount的实际值已经被ISR改变,这个变化也不会反映到主循环中使用的值上。

  3. 优化掉看似“无用”的读取
    编译器的另一个可能的优化是完全优化掉对tickCount的读取操作,特别是在它认为这个变量值未被修改的情况下。这可能导致程序逻辑上完全忽略了ISR对变量的更新。

volatile的作用

  • 防止编译器优化:编译器为了提高性能,可能会进行一些优化操作,比如将变量缓存在寄存器中,或者省略看似多余的读写操作。使用volatile可以禁止这些优化,确保每次访问都直接读取内存中的最新值。
  • 增加程序的可移植性:在不同的编译器和优化级别下,保证对易变变量的访问都是一致的。
  • 确保多线程程序的数据同步:在某些情况下,可以使用volatile来确保在多线程环境下共享变量的访问不会被编译器优化掉。但是,需要注意的是,volatile并不能替代线程同步机制(如互斥锁),它不保证原子性和内存可见性问题。

extern关键字

extern关键字在C和C++中用于声明一个变量或函数是在别的文件中定义的,目的是为了在当前文件中能够访问到它。使用extern可以在多个文件之间共享全局变量或函数。

用途和作用

  • 共享全局变量:当你有一个全局变量在一个文件中定义,并希望它能在其他文件中被访问和修改时,可以在其他文件中使用extern关键字来声明这个全局变量。这样,多个文件就可以共享同一个全局变量的实例。
  • 共享函数声明:对于函数,extern可以用来在一个文件中声明另一个文件中定义的函数。这实际上是默认行为,所以在函数声明前使用extern关键字是可选的。不过,在某些情况下显式声明可以增加代码的可读性。

示例

我们有两个源文件:main.cpphelper.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;
}
  • ab都被声明为std::atomic<int>类型,这意味着对它们的所有操作都是原子的,因此是线程安全的。
  • copyValue函数首先使用load方法从b安全地读取其值到一个局部变量localCopy中,然后使用store方法将这个值安全地存储到a中。这两个步骤确保了即使在并发环境下,int a = b;的操作也是线程安全的。
  • main函数中,我们修改了b的值,然后创建了一个线程来执行opyValue函数,该函数将b的值复制到a。我们通过join等待线程完成操作,然后打印出a的值。
  • 31
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值