C++基础
一、访问权限:
C++通过public、protected、private三个关键字来控制成员变量和成员函数的访问权限。
1. public
访问权限:
- 在类的内部和外部都可以访问。
- 对外提供接口,表示该成员对外可见。
1 | class Example { |
2. protected
访问权限:
- 在类的内部可以访问,派生类中也可以访问,但在类的外部不能访问。
- 主要用于继承关系,表示该成员对派生类可见。
1 | class Example { |
3. private
访问权限:
- 只有在类的内部可以访问,外部不能访问,包括派生类中也不能直接访问。
- 主要用于隐藏实现细节,表示该成员对外不可见。
1 | class Example { |
4. 访问权限与继承:
-
公有继承(public inheritance): 基类中的公有成员和保护成员都是派生类可见的,私有成员在派生类中不可见。
1
2
3
4
5
6
7
8
9
10
11
12class Base {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};
class Derived : public Base {
// Derived 类中可以访问 publicVar 和 protectedVar,但不能访问 privateVar
}; -
私有继承(private inheritance): 基类中的公有成员和保护成员都成为派生类的私有成员,不可见。
1
2
3
4
5
6
7
8
9
10
11
12class Base {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};
class Derived : private Base {
// Derived 类中无法直接访问 Base 的任何成员
}; -
受保护继承(protected inheritance): 基类中的公有成员和保护成员都成为派生类的保护成员,不可见。
1
2
3
4
5
6
7
8
9
10
11
12class Base {
public:
int publicVar;
protected:
int protectedVar;
private:
int privateVar;
};
class Derived : protected Base {
// Derived 类中可以访问 publicVar 和 protectedVar,但不能访问 privateVar
};
二、C++ 三大特性
1. 继承:
- 定义: 继承是一种面向对象编程的机制,允许一个类(子类/派生类)基于另一个类(父类/基类)的定义构建。子类继承了父类的属性和方法,并可以在不重新编写原来的类的情况下扩展或修改这些功能。
- 功能:
- 实现继承: 子类获得基类的属性和方法的能力,无需额外编码。
- 接口继承: 子类使用属性和方法的名称,但必须提供实现。
- 可视继承: 子类使用基类的外观和实现代码的能力。
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22// 基类 - 人
class Person {
public:
std::string name;
int age;
void eat() {
std::cout << name << " is eating." << std::endl;
}
void sleep() {
std::cout << name << " is sleeping." << std::endl;
}
};
// 派生类 - 学生
class Student : public Person {
public:
void study() {
std::cout << name << " is studying." << std::endl;
}
};
2. 封装:
- 定义: 封装是将数据和代码捆绑在一起,避免外界直接访问和修改对象的内部状态。通过将数据成员声明为私有的,通过公共方法提供对数据的访问和修改。
- 功能: 封装提供了信息隐藏的机制,使得对象的内部实现对外部不可见,提高了程序的安全性和可维护性。
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 封装示例
class BankAccount {
private:
double balance;
public:
// 公共方法提供对私有成员的访问
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
double getBalance() const {
return balance;
}
};
3. 多态:
- 定义: 多态性允许对象在运行时表现出不同的行为。在C++中,多态性通过虚函数和继承实现。可以在基类中声明虚函数,在派生类中重新定义该虚函数,以便在运行时调用正确的函数。
- 功能: 多态性提供了一种机制,可以根据对象的实际类型来选择调用相应的方法,而不是在编译时确定。
- 实现方式:
- 覆盖(Override): 子类重新定义基类的虚函数。
- 重载(Overload): 允许存在多个同名函数,但参数表不同。
- 示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 基类 - 动物
class Animal {
public:
virtual void makeSound() const {
std::cout << "Animal makes a sound." << std::endl;
}
};
// 派生类 - 狗
class Dog : public Animal {
public:
void makeSound() const override {
std::cout << "Dog barks." << std::endl;
}
};
// 派生类 - 猫
class Cat : public Animal {
public:
void makeSound() const override {
std::cout << "Cat meows." << std::endl;
}
};
这些概念一起构成了面向对象编程的三大特征:继承、封装和多态。它们帮助提高代码的可维护性、可扩展性和重用性。
三、数据类型
1. 整型数据类型
- short: 至少16位。通常为2字节。
- int: 至少与
short
一样长。通常为4字节。 - long: 至少32位,且至少与
int
一样长。通常为4字节。 - long long: 至少64位,且至少与
long
一样长。通常为8字节。
2. C++ 整型数据长度标准
-
1 byte = 8 bit: 在大多数系统中,1字节等于8位。
-
系统最小长度: 很多系统使用最小长度,例如:
short
通常为16位(2字节)。long
通常为32位(4字节)。long long
通常为64位(8字节)。int
长度较为灵活,通常被认为为4字节,与long
等长。
3. sizeof
运算符
sizeof
运算符用于获取数据类型或对象的大小(以字节为单位)。- 通过以下示例,可以显示不同数据类型的大小:
1
2cout << "int is " << sizeof(int) << " bytes. \n";
cout << "short is " << sizeof(short) << " bytes. \n";
4. climits
头文件
climits
头文件定义了一些整型数据类型的符号常量,如:INT_MAX
:表示int
的最大值。INT_MIN
:表示int
的最小值。
5. 无符号类型
- 无符号整型 (
unsigned
) 不存储负数值,可以增大变量能够存储的最大值,但数据长度不变。 - 例如,
unsigned int
可以存储从0到2^32 - 1
的非负整数。
选择数据类型
int
被设置为自然长度,即计算机处理效率最高的长度,因此在选择数据类型时,通常选择int
类型。
这些规定和概念有助于程序员理解和选择适当的整型数据类型,以满足其需求并保证代码的可移植性。
四、指针与引用
指针与引用是C++中两种不同的概念,它们分别用于处理内存地址和创建变量别名。
1. 指针(Pointer)
- 存放对象地址: 指针是一个变量,其存放的是某个对象的内存地址。
- 可变性: 指针本身可以改变,即可以指向不同的内存地址。包括指向的地址的改变和指向的地址中所存放的数据的改变。
- 指向指针的指针: 指针本身也有地址,因此可以有指向指针的指针。
- 需要初始化: 在使用指针之前,需要先初始化,通常通过将其指向一个对象的地址来实现。
1 | int x = 10; |
int *ptr
: 定义了一个指向整数的指针。&x
: 取地址运算符,返回变量x
的地址。
2. 引用(Reference)
- 变量的别名: 引用是变量的别名,它引用(代表)了一个已存在的对象。
- 不可变性: 一旦引用被初始化,它将一直引用同一个对象,不可变。引用必须在初始化时指定引用的对象,之后不能更改。
- 不存在空引用: 引用不存在指向空值的情况,它必须引用一个已存在的对象。
1 | int x = 10; |
int &ref
: 定义了一个整数的引用。x
: 引用的是变量x
。
3. 解引用(Dereference):
解引用是通过指针或引用访问其指向的内存地址中存储的值。
1 | int x = 10; |
*ptr
: 解引用指针ptr
,得到存储在ptr
指向的内存地址中的值。
4. 取址符(Address-of Operator):
取址符(&
)用于获取变量的内存地址。
1 | cppCopy code |
&x
: 取得变量x
的地址,将其赋给指针ptr
。
5. 指针与引用的比较
指针的特性
- 可以为空:指针可以指向
nullptr
,表示它不指向任何对象。 - 可以重新赋值:指针可以在其生命周期内指向不同的对象。
- 支持算术运算:指针可以进行算术运算,如递增、递减等。
- 需要解引用:访问指针所指向的对象时需要使用
*
操作符。 - 内存占用:指针本身占用内存(通常为4或8字节,取决于系统架构)。
引用的特性
- 不能为空:引用必须在声明时初始化,且不能为
nullptr
。 - 不可重新赋值:引用在其生命周期内始终引用同一个对象。
- 不支持算术运算:引用不能进行指针算术运算。
- 无需解引用:引用直接访问对象,无需使用
*
操作符。 - 内存占用:引用本身不占用额外内存,只是原变量的一个别名。
1 | int x = 10; |
五、关键字
1. const 关键字
const
是 C++ 中用于声明常量的关键字,它可以用于不同的上下文中,限定变量、指针、引用等的特性。
1. const 修饰变量(常量):
1 | const int max_value = 100; // 定义一个常量 max_value |
在这里,max_value
和 pi
都是常量,它们的值不能被修改。
2. const 修饰指针:
-
底层 const(常量指针):
定义:具有只能够读取内存中数据,却不能够修改内存中数据的属性的指针,称为指向常量的指针,简称常量指针。
1
2
3
4int temp = 10;
// a是一个常量指针,它指向一个只读的对象,不能通过a修改temp的值。
const int* a = &temp;
int const* a = &temp; -
顶层 const(指针常量):
定义:指针常量是指指针所指向的位置不能改变,即指针本身是一个常量,但是指针所指向的内容可以改变。
注:指针常量必须在声明的同时对其初始化,不允许先声明一个指针常量随后再对其赋值,这和声明一般的常量是一样的。
1
2
3
4int temp = 10;
int temp2 = 20;
// p是一个指针常量,它的值只能在初始化时指向temp,其他地方不能改变。即p本身不能指向其他地址。
int* const p = &temp;
注意:
const
关键字的位置决定是底层 const 还是顶层 const。const
修饰指针时,要根据const
的位置分清是底层 const 还是顶层 const。
2. Define 和 Typedef 、Inline的区别:
Define:
- 定义方式: 使用
#define
关键字,将标识符与一个表达式或者语句进行关联。 - 类型检查: 无类型检查,仅进行简单的字符串替换。
- 作用范围: 在预处理阶段进行替换,是一种文本替换机制。
- 常用场景: 常用于定义宏,进行代码中的文本替换,如宏定义常量、条件编译等。
- 使用灵活性: 没有对应的数据类型,可以定义任何文本。
1 |
Typedef:
- 定义方式: 使用
typedef
关键字,为一个已有的数据类型取一个新的名字。 - 类型检查: 提供类型检查,用于定义新的类型别名。
- 作用范围: 在编译和运行时生效,具有对应数据类型。
- 常用场景: 用于提高代码的可读性,为复杂的类型起一个更简洁的名字。
- 使用灵活性: 可以对各种类型进行别名定义。
1 | typedef int Integer; // Integer 是 int 的别名 |
Inline:
inline是先将内联函数编译完成生成了函数体直接插入被调用的地方,减少了压栈,跳转和返回的操作。没有普通函数调用时的额外开销;
- 定义方式: 使用
inline
关键字,对函数进行内联展开。 - 类型检查: 提供类型检查,是真正的函数调用。
- 编译时机: 在编译时进行内联展开,替代函数调用。
- 常用场景: 适用于函数体简单、频繁调用的场景,提高执行效率。
1 | inline int square(int x) { |
C++中inline编译限制:
- 不能存在任何形式的循环语句
- 不能存在过多的条件判断语句
- 函数体不能过于庞大
- 内联函数声明必须在调用语句之前
总体而言,typedef
用于创建类型别名,提高代码的可读性和可维护性;define
用于进行简单的文本替换,例如宏定义;inline
用于函数的内联展开,提高执行效率。每个关键字在不同的场景下都有其独特的作用。
3. Override 和Overload
Override(重写):
-
定义: 在子类中定义一个与父类中具有相同名称和参数列表的方法,以实现不同的功能。
-
规则:
- 重写方法的参数列表、返回值、所抛出的异常应与被重写方法一致。
- 被重写的方法不能为
private
。 - 静态方法不能被重写为非静态的方法。
- 重写方法的访问修饰符一定要大于等于被重写方法的访问修饰符(
public
>protected
>default
>private
)。
-
使用场景: 主要用于子类继承父类时,根据需要重新实现父类的方法。
Overload(重载):
-
定义: 在同一个类中,可以有多个同名的方法,但它们的参数列表不同,以实现不同的功能。
-
规则:
- 不能通过访问权限、返回类型、抛出的异常进行重载。
- 不同的参数类型可以是不同的参数类型、不同的参数个数、不同的参数顺序(参数类型必须不一样)。
- 方法的异常类型和数目不会对重载造成影响。
-
使用场景: 主要用于一个类中,为同一方法提供不同的版本,以适应不同的参数情况。
本质区别:
- 加入了
override
修饰符的方法,在使用时始终只有一个被使用。这是因为override
用于明确表示这是对父类方法的重写,编译器会在编译时检查确保符合规则。而重载则是在同一类中,可以存在多个同名方法,根据不同的参数调用不同的版本。
使用多态:
- 使用重写和重载的目的是为了实现多态性,提高代码的灵活性和可维护性。多态性允许以一种统一的方式对待不同的对象,通过基类指针或引用调用相同的方法,实现对不同派生类对象的统一操作。
重新生成一下解释,并提供一个小例子:
4. new和 malloc
-
内存分配失败时的处理:
new
: 当new
无法分配所需的内存时,会抛出std::bad_alloc
异常。malloc
: 当malloc
无法分配所需的内存时,会返回NULL
。
-
指定内存块大小:
new
: 无需显式指定内存块的大小,系统会自动计算。malloc
: 需要显式指定所需内存的大小。
-
重载:
new
:operator new
和operator delete
可以被重载。malloc
:malloc
和free
不能被直接重载。
-
构造函数和析构函数的调用:
new
:new
操作符会调用对象的构造函数,delete
操作符会调用对象的析构函数。malloc
:malloc
和free
不会调用对象的构造和析构函数。
-
标准库和语言:
new
: C++运算符,用于动态内存管理。malloc
: C语言标准库函数,在C++中也可用。
-
内存分配来源:
new
: 从自由存储区(free store)上为对象动态分配内存空间。malloc
: 从堆(heap)上动态分配内存。
5. constexpr
和 const
:
const
(常量):
-
语义: 表示“只读”的语义,用于声明不可修改的变量。
-
运行期和编译期常量: 可以定义编译期常量,也可以定义运行期常量。
-
成员函数和变量: 将成员函数标记为
const
表示该函数不会修改对象的状态;将变量标记为const
表示该变量的值不可修改。 -
指针和
const
:- 指针常量:
const int* d = new int(2);
表示指向常量的指针。 - 常量指针:
int *const e = new int(2);
表示常量指针,指针本身是常量。
- 指针常量:
-
const
的修改: 若要修改const
修饰的变量的值,需要加上关键字volatile
。 -
const
成员函数和mutable
:const
成员函数:表示该函数不会修改对象的状态。mutable
关键字:用于修饰某些与类状态无关的数据成员,允许在const
成员函数中修改这些成员。
constexpr
(常量表达式):
-
语义: 表示“常量”的语义,用于定义编译期常量。
-
常量初始化: 必须使用常量初始化,例如:
1
2
3constexpr int n = 20;
constexpr int m = n + 1;
static constexpr int MOD = 1000000007; -
指针和
constexpr
: 如果constexpr
声明中定义了一个指针,仅对指针有效,和所指对象无关,例如:1
constexpr int *p = nullptr; // 常量指针(顶层 const)
constexpr
关键字意味着p
在编译时是一个常量,这在C++11及更高版本中被引入,用于编译时常量表达式。对于指针来说,constexpr
指针意味着指针本身是一个常量,且必须初始化。constexpr
指针通常用于在编译时需要已知值的场景,如数组大小、模板参数等。1
const int *q = nullptr; // 指向常量的指针(底层 const)
const
关键字在指针声明中的位置不同,会有不同的含义。对于const int *q
,const
修饰的是int
,表示指向常量的指针。这种指针用于需要保护数据不被修改的场景,常见于接口参数,确保函数不修改传入的数据。1
int *const r = nullptr; // 顶层 const 常量指针
当
const
关键字放在指针声明的后面,修饰的是指针本身,表示指针是常量,但指针指向的值可以改变。常量指针用于保证指针本身的地址不变,但允许修改指针指向的数据。常用于需要固定地址的指针,如引用某个固定的缓冲区。 -
constexpr
函数:- 用于常量表达式的函数,返回类型和所有形参类型都是字面值类型。
- 函数体有且只有一条
return
语句。 - 为了在编译过程展开,
constexpr
函数被隐式转换成了内联函数。
-
constexpr
构造函数:- 构造函数不能被声明为
const
,但字面值常量类的构造函数可以是constexpr
。 - 必须有一个空的函数体,所有成员变量的初始化都放到初始化列表中。
- 对象调用的成员函数必须使用
constexpr
修饰。
- 构造函数不能被声明为
const
和 constexpr
的区别:
-
const
表示只读,可以定义运行期常量,而constexpr
表示常量,只能定义编译期常量。 -
constexpr
变量在复杂系统中可以用于明确表达初始值是常量表达式,由编译器验证变量的值是否是常量表达式。 -
constexpr
函数被隐式转换成内联函数,可以在编译过程中展开,提高效率。相比宏,constexpr
更安全可靠,并且没有额外的开销。 -
如果将函数或变量标记为
constexpr
,它同样是const
的,但相反并不成立。一个const
的变量或函数并不一定是constexpr
的。
6. volatile
:
定义:
volatile
是一个类型修饰符,与 const
绝对对立。它影响编译器对变量的编译结果,用于声明的变量表示该变量随时可能发生变化,与该变量有关的运算不要进行编译优化。使用 volatile
关键字声明的变量会导致每次访问时都从内存中重新加载内容,而不是直接从寄存器拷贝内容。
作用:
- 保证本条指令不会因编译器的优化而省略。
- 要求每次直接读取值,确保对特殊地址的稳定访问。
使用场合:
- 在中断服务程序和与 CPU 相关寄存器的定义,以确保对这些变量的访问不会受到编译器的优化影响。
举例说明:
编译器优化是为了生成更高效的代码,减少程序的运行时间和资源消耗。当编译器发现一个空循环,即循环体中没有任何对程序有实际影响的操作时,它会认为这个循环没有任何意义,因此可能会将其优化掉,以提高程序的效率。
volatile
告诉编译器,这个变量可能会在程序的其他地方以不可预测的方式被修改,因此每次访问都必须实际读取内存,从而防止优化。
1 | // 示例:空循环,使用 volatile 防止被优化掉 |
7. extern
:
定义:
extern
用于声明外部变量,即在函数或文件外部定义的全局变量。
8. 静态变量、全局变量和局部变量:
静态变量(Static Variable)
- 定义:在函数或类中使用
static
关键字声明的变量。 - 生命周期:从程序开始到程序结束。
- 作用域:如果在函数内声明,作用域是函数内;如果在类中声明,作用域是类的实例。
- 内存分布:存储在数据段(Data Segment)。
- 特点:
- 在函数中声明的静态变量在函数调用结束后不会销毁,下次调用时保持上次的值。
- 在类中声明的静态成员变量是所有对象共享的。
静态局部变量
1 | void functionWithStatic() { |
静态全局变量
1 | // file1.cpp |
全局变量(Global Variable)
- 定义:在所有函数外部声明的变量。
- 生命周期:从程序开始到程序结束。
- 作用域:整个程序(所有文件),但可以用
static
限制其作用域在声明的文件内。 - 内存分布:存储在数据段(Data Segment)。
- 特点:
- 任何函数都可以访问和修改全局变量。
- 易引起命名冲突和难以维护。
1 | // file1.cpp |
局部变量(Local Variable)
- 定义:在函数或块内声明的变量。
- 生命周期:从声明开始到所在的块或函数结束。
- 作用域:声明所在的块或函数。
- 内存分布:存储在栈区(Stack)。
- 特点:
- 只能在声明的块或函数内访问。
- 使用局部变量不会污染全局命名空间。
1 |
|
static
关键字作用:
-
静态变量(函数内部):
-
静态变量(类内部):
-
静态函数:
- 只能访问类的静态成员变量。
- 不能访问类的非静态成员变量。
1
2
3
4
5
6class Example {
public:
static void func() {
// 静态函数只能访问静态成员变量
}
}; -
静态全局变量:
- 作用域限制在声明的文件内部。
- 其他文件不能直接访问。
1
static int globalVar = 10;
const
关键字作用:
1. 常量变量
定义:用 const
修饰的变量,值不能修改。
示例:
1 | const int a = 10; |
2. 常量指针(Pointer to const)
定义:指针指向的是常量数据,不能通过指针修改数据。
示例:
1 | int value = 10; |
3. 指针常量(Const pointer)
定义:指针本身是常量,即指针所指向的地址不可变。
示例:
1 | int value = 10; |
4. 常量成员函数
定义:表示函数不修改类的成员变量。
示例:
1 | class Example { |
5. 常量引用(Reference to const)
定义:不能通过引用修改引用的对象。
示例:
1 | int value = 10; |
6. 常量参数
定义:用于函数参数,防止函数内部修改传入的参数值。
示例:
1 | void func(const int param) { |
7. 返回值常量
定义:用于函数返回值,防止调用者修改返回的对象。
示例:
1 | const int getValue() { |
const
关键字的主要作用是保护数据不被意外修改,提高代码的安全性和可读性。根据应用场景,const
可以修饰变量、指针、引用、成员函数、参数等。通过合理使用 const
,可以确保代码中某些值的不可变性,从而减少潜在的错误。
区别与使用场景
static
用于控制变量的生命周期、作用域以及类成员的共享性。const
用于防止变量或对象被修改,增强代码的安全性和稳定性。
示例
1 |
|
9. 前置++与后置++
在 C++ 中,++
运算符可以被重载以支持前置和后置递增操作。
举例说明
1 |
|
- 返回对象
const self
,表示后置++会返回原对象的值,并在原对象上执行递增操作。 - 参数
int
是用于区分前后置++的占位参数,编译器在调用时为其提供一个默认值(通常是0)。
-
为什么后置++返回对象而不是引用?
- 后置++需要返回原对象的值,但为了保持语言的一致性,也就是为了与内置类型的行为保持一致,返回一个值而不是引用。这是为了防止在后置++中的连续调用中产生不可预测的结果,因为后置++首先返回原值,然后再进行递增。
-
为什么后置++前面要加 const?
- 在内置类型中,后置++返回一个值,而不是引用,这与内置类型的行为是一致的。为了与内置类型行为一致,防止用户连续两次调用后置++,比如
i++++
,在第一次后置++后返回的是旧值,而不是原对象,所以为了防止连续调用,通过在后置++函数前面加上const
可以禁止其合法化。
- 在内置类型中,后置++返回一个值,而不是引用,这与内置类型的行为是一致的。为了与内置类型行为一致,防止用户连续两次调用后置++,比如
-
处理用户的自定义类型为什么最好使用前置++?
- 前置++不会创建临时对象,直接在原对象上进行递增操作,避免了构造和析构带来的额外开销。在性能要求较高的情况下,使用前置++更为高效。
在C++中,a++
和 int a = b
这两种操作在多线程环境下并不是线程安全的。原因在于这些操作涉及到对变量的读取和修改,而在多线程环境下,多个线程可能同时对同一变量进行读写操作,从而导致竞态条件和数据不一致的问题。
10. std::atomic
std::atomic
是 C++11 标准引入的一个模板类,用于实现多线程并发编程中的原子操作。它提供了一种线程安全的方式来操作共享的内存变量,避免了竞态条件(race condition)和数据竞争(data race)的问题。
-
原子操作:
atomic
类模板提供了多种原子操作,包括加载、存储、交换、比较和交换等,这些操作能够在不需要额外的同步机制(如互斥锁)的情况下,确保对共享变量的安全访问。 -
数据类型:
atomic
可以用于支持标量类型(如整数、浮点数、指针等),也可以用于支持某些特定类型的自定义类型(通过特化atomic
模板)。 -
内存模型:
atomic
保证了操作的原子性,即这些操作在执行时不会被中断,其他线程也不会在这些操作中间修改同一变量,从而避免了竞态条件和数据竞争。 -
线程安全:使用
atomic
可以避免手动编写复杂的同步代码,提高了编程的简洁性和可读性,同时提升了多线程程序的性能。
11. 问题:a++和 int = a + b是否是线程安全的?
a++
对于 a++
操作,它实际上包含了读取、递增和写回三个步骤。即使从语法层面看,这是一个语句,应该是原子的,但是在底层的汇编指令中,这三个步骤并不是原子执行的。以下是可能的汇编指令:
1 | mov eax, dword ptr [a] // 读取a的值 |
在多线程环境下,两个线程同时执行 a++
操作可能会导致竞态条件,因为两个线程读取并递增的时候可能会相互干扰,最终结果可能不是期望的。
int a = b
对于 int a = b
操作,它涉及到将变量 b
的值读取到一个寄存器中,然后将寄存器中的值写入变量 a
所代表的内存地址。这也可能不是原子操作,因为读取和写入是两个步骤。以下是可能的汇编指令:
1 | mov eax, dword ptr [b] // 读取b的值到寄存器 |
在多线程环境下,两个线程同时执行 int a = b
操作可能会导致一个线程读取 b
的值,而在写回 a
的时候被另一个线程中断,最终结果可能不是期望的。
C++11 引入了 std::atomic
,这是一个模板类型,提供了对整数类型的原子操作。使用 std::atomic
可以确保对变量的读写操作是原子的,从而避免了竞态条件和数据不一致的问题
1 |
|
在上面的示例中,std::atomic<int>
类型的 counter
变量被两个线程同时递增,由于使用了原子变量,不需要额外的同步手段,程序能够避免竞态条件。
六、C++强制类型转换
在 C++ 中,有四个强制类型转换关键字:static_cast
、dynamic_cast
、reinterpret_cast
和 const_cast
。每个关键字都有其特定的用途和限制。以下是对这四个关键字的详细解释:
1. static_cast
- 用途:
static_cast
主要用于进行静态类型转换。 - 特点:
- 没有运行时类型检查,所以在转换的安全性上需要开发者来保证。
- 可以进行上行转换(派生类指针或引用转换为基类表示),是安全的。
- 进行下行转换(基类指针或引用转换为派生类表示)是不安全的,因为没有动态类型检查。
- 使用场景:
- 用于基本数据类型之间的转换,如将
int
转换为char
。 - 用于将任何类型的表达式转换为
void
类型。
- 用于基本数据类型之间的转换,如将
1 |
|
2. dynamic_cast
- 用途:
dynamic_cast
用于进行动态类型转换,主要用于在继承关系中的类型转换。 - 特点:
- 有运行时类型检查,因此对于下行转换更安全。
- 可以在存在虚函数的父子类关系中进行强制类型转换。
- 对于指针,转换失败返回
nullptr
;对于引用,转换失败会抛出异常。
- 使用场景:
- 在进行基类指针或引用到派生类的转换时,提供了安全性保证。
- 用于类之间的交叉转换,要求基类必须有虚函数。
1 |
|
3. reinterpret_cast
- 用途:
reinterpret_cast
主要用于进行底层的、不安全的类型转换,通常用于指针和引用的转换。 - 特点:
- 允许将整数类型转换为指针,也允许将指针转换为整数类型。
- 可以在指针和引用之间进行较为自由的转换,但平台移植性较差。
- 使用场景:
- 用于进行底层的位操作,例如将指针转换为整数或反之。
1 |
|
4. const_cast
- 用途:
const_cast
主要用于添加或移除变量的const
或volatile
限定符。 - 特点:
- 用于去除类型的
const
或volatile
属性。 - 常量指针可以被转换为非常量指针,常量引用可以被转换为非常量引用。
- 用于去除类型的
- 使用场景:
- 用于在特定情况下去除常量性,以方便修改变量的值。
1 |
|
总体而言,这些强制类型转换关键字应该谨慎使用,特别是 reinterpret_cast
,因为它在类型转换上非常灵活但也非常危险。在实际使用中,应根据具体场景选择适当的转换方式,同时注意类型安全性和代码的可读性。
运算符重载是 C++ 中的一个重要特性,允许用户自定义类型在运算符上的行为。以下是关于运算符重载的详细解释:
七、重载运算符基本概念
- 函数 : 重载运算符本质上是函数调用,使得用户定义的类型可以在特定的运算符上像内置类型一样工作。
- 两种调用方式:
- 使用运算符的方式,例如
data1 + data2
。 - 使用函数调用的方式,例如
operator+(data1, data2)
。
- 使用运算符的方式,例如
2. 可以重载的运算符
在 C++ 中,有一些运算符是可以被重载的,包括逗号、取地址、逻辑与和逻辑或等。不过,有一些运算符不建议重载,如逗号、取地址等,因为它们本身对类类型有特殊定义,而逻辑与、逻辑或具有短路求值属性,重载可能引起不符合预期的结果。
3. 运算符重载的形式
运算符重载可以是类的成员函数或非成员函数。一些建议如下:
-
成员函数形式: 对于逻辑上属于某个类的操作,可以使用成员函数形式。例如,对于复数类
Complex
,可以使用成员函数形式定义加法运算符。1
2
3
4
5
6class Complex {
public:
Complex operator+(const Complex& other) const {
return Complex(real + other.real, imag + other.imag);
}
}; -
非成员函数形式: 对于逻辑上不属于某个类的操作,可以使用非成员函数形式。例如,对于字符串拼接,可以定义非成员函数形式的加法运算符。
1
2
3
4
5
6
7class String {
// ...
};
String operator+(const String& str1, const String& str2) {
// 拼接 str1 和 str2
}
4. 重载运算符的注意事项
- 参数数量: 重载运算符的参数数量和类型是固定的,对于二元运算符,有两个参数。
- 成员函数参数: 如果重载运算符是类的成员函数,左侧运算对象相当于固定为
this
,右侧运算对象是传递进来的参数。 - 友元和私有成员: 如果需要访问类的私有成员,可以将非成员函数形式的运算符重载声明为友元函数。
5. 一些建议规则
- 算术和关系运算符建议非成员: 这些运算符是对称的,建议将它们定义为非成员函数。
- 赋值运算符必须是成员: 赋值运算符必须修改调用对象的状态,因此必须是成员函数。
- 下标运算符必须是成员: 下标运算符必须是成员函数,因为它用于访问类对象的元素。
- 递增递减运算符建议成员: 这些运算符修改对象状态,建议将它们定义为成员函数。
6. 函数调用运算符
函数调用运算符 operator()
允许对象像函数一样被调用。这对于函数对象(例如 Lambda 表达式)非常有用,因为编译器将 Lambda 表达式翻译为一个未命名类的未命名对象。函数调用运算符重载的形式如下:
1 | class Functor { |
以上是关于运算符重载的一些基本概念和注意事项,具体的重载形式和规则取决于特定
的运算符和使用场景。