[C++ Basic] Grammar
概要
C++ 基础知识和语法,包括C++11,C++17,C++23的各种语言支持。
C++编程语言历史 和 设计思路
C 与 C++、java 的 区别¶
支持范式模板编程 (generic programming)
- 模板代码(增加泛型编程能力,类似python),
- 泛型编程是一种以通用性为中心的编程范式。在泛型编程中,程序通过使用参数化类型(或称为模板)来实现数据类型无关的算法和数据结构
- 强⼤的 Standard Template Library (STL) 标准库, 也是基于泛型编程的产物。
- 包括:容器、迭代器、算法和函数对象 四类
- 元编程(e.g., constexpr )编译时推导出变量是否为固定常数。
- 一些语法和关键字,增加了 new 和 delete,auto
支持面向对象编程 (object-oriented programming) 的拓展
- 类和对象:C++允许定义类和创建对象。类是一种用户自定义的数据类型,可以包含成员变量和成员函数。对象是类的一个实例,可以通过类来创建多个对象。C语言中没有类和对象的概念,只能使用结构体和函数来组织数据和行为。
- 封装:C++支持封装,可以将数据和相关的操作封装在一个类中,并使用访问修饰符来控制对类成员的访问权限。C语言没有封装的概念,所有的数据和函数都是公开的。
- 继承:C++支持继承,允许创建派生类从基类继承属性和行为。继承可以实现代码重用和类的层次化。C语言没有继承的概念。
- 多态:C++支持多态,允许通过基类指针或引用来调用派生类的虚函数,实现动态绑定和运行时多态性。C语言没有多态的概念。
- 异常处理:C++提供异常处理机制,可以通过抛出和捕获异常来处理程序中的错误和异常情况。C语言没有内置的异常处理机制。
C++ 与 java 的区别
- 内存管理:C++中的内存管理是手动的,程序员需要显式地分配和释放内存。C++提供了new和delete关键字来进行动态内存分配和释放。Java中的内存管理是自动的,使用垃圾回收机制来自动管理内存,程序员不需要手动释放内存。
- 指针:C++支持指针操作,允许直接访问和修改内存地址。Java中没有指针的概念,所有的数据访问都是通过引用进行的。
- 运行环境:C++是一种编译型语言,源代码在编译后被转换为机器码,并直接在操作系统上运行。Java是一种解释型语言,源代码在编译后生成字节码,然后由Java虚拟机(JVM)解释执行。
-
平台依赖性:C++代码在不同的平台上需要重新编译,因为它直接与底层系统交互。Java代码是平台无关的,一次编译的字节码可以在任何支持Java虚拟机的平台上运行。
-
C++更适合系统级编程、游戏开发等需要更高的性能和底层控制的场景。
- Java更适合企业级应用开发、网络编程等需要跨平台和可移植性的场景。
基础知识与坑¶
程序执行入口¶
The default program entry function is main
, but can be changed in two situations:
- use stupid
#define xxx main
in header file to replace the name which maybe ignored by silly search bar in VSCODE. - use
-exxx
compile flag
语句末尾的分号¶
- 在 C++ 中,是否需要在语句的末尾使用分号(
;
)取决于上下文。 -
C++ 语法的基本规则是:分号用来标识一条语句的结束,而有些结构并不是严格的语句,因此不需要分号。
-
声明(变量、类、结构体、枚举、函数原型、类型别名):都需要分号作为结束。
- 函数定义、控制语句(如
if
,while
,for
)、复合语句({}
) 不需要分号。 - 预处理指令(如
#define
,#include
):不需要分号,因为它们不是 C++ 语法层面的内容。 - 作用域结束的
}
不需要分号,但声明类或结构体时}
后要加分号。
为什么类/结构体需要分号
C++ 中的类和结构体定义实际上是一种声明,它们的定义是一种复杂的声明语句,因此必须用分号来结束它们。
总结来说,分号用来结束语句,包括声明、表达式和执行体等,但当你定义一个复合结构(如函数定义、控制语句)时,不需要分号来结束复合结构的定义。
逗号运算符(Comma)¶
重名变量的优先级¶
int getKthAncestor(int node, int k) {
int node= getKthAncestor(saved[node],--k);
return node;
}
//为什么第二行的node会提前被修改为0,导致传入函数getKthAncestor的saved[node]的node值为0
//如下去掉int,也不会错。因为int node 会初始化node为0
int getKthAncestor(int node, int k) {
node= getKthAncestor(saved[node],--k);
return node;
}
根据C++的作用域规则,内层的局部变量会覆盖外层的同名变量。因此,在第二行的语句中,node引用的是函数参数中的node,而不是你想要的之前定义的node。
为了避免这个问题,你可以修改代码,避免重复定义变量名。例如,可以将第二行的变量名改为newNode或其他不同的名称,以避免与函数参数名冲突。
运算符优先级¶
运算符性质:
- 接受的操作数,
- 优先级,
- 特殊:逻辑和(&&)先于逻辑或(||)、四则运算先于位运算
- 位运算优先级低于判断符号,记得写括号。
- 赋值(=)优先级最低
- 结合性,
- 左结合性: 大部分运算(加减乘除)
- 右结合性:赋值运算符。程序会先计算它右边的表达式的值,然后再计算它左边的表达式的值
- 返回值
- 赋值运算符的返回值是赋值后左操作数的引用
变量类型以及Macro constants¶
https://en.cppreference.com/w/cpp/language/types
https://en.cppreference.com/w/cpp/types/integer
//返回与平台相关的数值类型的极值
std::numeric_limits<double>::max()
std::numeric_limits<int>::min()
#include<limits.h>
INT_MAX
FLT_MAX (or DBL_MAX )
-FLT_MAX (or -DBL_MAX )
关键词¶
extern
const
constexpr //C++11引入的关键字,用于编译时的常量与常量函数。
volatile //是指每次需要引用某个变量的数据时,都必须从内存原地址读取,而不是编译器优化后寄存器间接读取.(必须写回内存,为了多进程并发而设计的。)
inline
static 关键字¶
static 作⽤:控制变量的存储⽅式和作用范围(可⻅性)。
- 修饰局部变量
- 存放位置:栈区 -> 静态数据区(data段或者bss段)
- 生命周期:程序结束才会释放
- 作用域:还是局部代码块
- 修饰函数与全局变量
- 使其作用范围由全工程文件可见变成了本文件可见
多文件共用static变量,需要添加 extern 关键字, 去掉static关键字
在 C++ 中,如果你希望 static
变量可以在多个文件中访问,直接写在头文件中是 不正确的,因为每个包含该头文件的源文件都会生成自己的独立 static
变量,导致它们互相独立,无法共享状态。
正确的解决方案
- 如果需要共享(全局变量的方式)
你应该将
encounteredAclops
声明为extern
变量,并将其定义在一个.cpp
文件中。
头文件:globals.h
源文件:globals.cpp
在其他源文件中使用:
#include "globals.h"
void someFunction() {
if (!encounteredAclops) {
// Do something
encounteredAclops = true;
}
}
通过 extern
,所有引用 encounteredAclops
的源文件都会共享同一个变量。
- 如果每个文件需要独立的变量
如果每个源文件都需要独立的 encounteredAclops
,你可以将 static bool encounteredAclops = false;
放在各自的源文件中,而不需要放在头文件中。这是因为 static
的作用域仅限于当前编译单元。
每个源文件:
static bool encounteredAclops = false;
void someFunction() {
if (!encounteredAclops) {
// Do something
encounteredAclops = true;
}
}
- 如果需要在类中管理(推荐做法)
可以考虑将
encounteredAclops
作为一个类的静态成员变量来实现共享状态。
头文件:AclopsManager.h
#ifndef ACLOPS_MANAGER_H
#define ACLOPS_MANAGER_H
class AclopsManager {
public:
static bool encounteredAclops;
};
#endif // ACLOPS_MANAGER_H
源文件:AclopsManager.cpp
在其他源文件中使用:
#include "AclopsManager.h"
void someFunction() {
if (!AclopsManager::encounteredAclops) {
// Do something
AclopsManager::encounteredAclops = true;
}
}
这种方式既可以共享变量,又能保持代码组织清晰。
结论
- 如果需要共享变量:使用 extern
或类的静态成员变量。
- 如果需要独立变量:将 static
声明放在各自的源文件中。
- 不要直接在头文件中定义 static bool encounteredAclops
,否则会导致每个包含头文件的源文件都生成自己的副本,违背初衷。
- 修饰类内函数
- 静态成员函数:使用"static"修饰的成员函数称为静态成员函数。静态成员函数与类的对象无关,可以在没有创建对象的情况下直接通过类名调用。这意味着它们不需要通过类的对象来访问,而是属于整个类的。举例
- 静态成员函数没有隐式的this指针,因此不能直接访问非静态成员变量和非静态成员函数。静态成员函数可以访问类的静态成员变量和其他静态成员函数。
- static 成员函数不能被 virtual 修饰, static 成员不属于任何对象或实例,所以加上 virtual没有任何实际意义;
- 静态成员函数没有 this 指针,虚函数的实现是为每⼀个对象分配⼀个vptr 指针,⽽ vptr 是通过 this 指针调⽤的,所以不能为 virtual;虚函数的调⽤关系,this->vptr->ctable->virtual function。
- 修饰类内的变量
- 存放位置:栈区 -> 静态数据区(data段或者bss段)
- 生命周期:程序结束才会释放
- 意味着下一次调用函数时,静态局部变量将保持上一次调用时的值。
- 由于不再属于某个类对象,可以直接通过类名初始化
int MyClass::staticVariable = 10;
多线程场景,修饰局部变量,会导致多线程共用,建议使用thread_local
来避免竞争
static
局部变量在第一次被访问时初始化,且初始化过程是线程不安全的。如果两个线程几乎同时首次访问这个变量,可能会导致初始化竞争,进而引发未定义行为。
static 修饰初始化命令,只会执行一次,无论是否多次经过
问题场景:
static auto thread_core_map = GetCpuAffinityMap(device_id);
即使程序多次调用函数经过这行,但是这行命令也只会执行第一次。但是如果我把static关键词去除,就正常执行多次了。
当在一行代码中使用了 static
关键字时,变量的初始化只会在它第一次被执行时进行,之后即使多次经过这行代码,初始化的代码块也不会被重复执行。
在例子中,thread_core_map
是一个静态局部变量,它在第一次经过时会调用 GetCpuAffinityMap(device_id)
函数并保存结果。在后续的函数调用中,即使再次经过这行代码,GetCpuAffinityMap(device_id)
不会被重新调用,因为 thread_core_map
已经被初始化过了。
去除 static
后,thread_core_map
会在每次经过这行代码时重新初始化,也就是每次都会调用 GetCpuAffinityMap(device_id)
。
解决方法:
如果你需要这行代码每次执行时都重新调用 GetCpuAffinityMap(device_id)
,那么应该去掉 static
关键字,或根据不同条件进行显式地重新初始化静态变量。
比如,可以这样实现惰性初始化或重置的功能:
static auto thread_core_map = GetCpuAffinityMap(device_id);
// 根据条件重新初始化
if (/* 条件 */) {
thread_core_map = GetCpuAffinityMap(device_id);
}
这样你就可以在特定条件下让 thread_core_map
被重新赋值。
如果你对静态变量的初始化行为没有问题,但是希望特定场景下重新执行初始化函数,可以根据场景调整条件逻辑。
静态成员函数
const 关键字¶
当const修饰基本数据类型时,可以将其放置在类型说明符的前面或后面,效果是一样的。const关键字用于声明一个常量,即其值在声明后不可修改。
当const关键字位于指针变量或引用变量的左侧时,它用于修饰指针所指向的变量,即指针指向的内容为常量。当const关键字位于指针变量或引用变量的右侧时,它用于修饰指针或引用本身,即指针或引用本身是常量。
-
修饰指针指向的变量, 它指向的值不能修改:
-
修饰指针本身 ,它不能再指向别的变量,但指向(变量)的值可以修改。:
-
const int *const p3;
//指向整形常量 的 常量指针 。它既不能再指向别的常量,指向的值也不能修改。
explicit¶
在C++, explicit 是一个关键字,用于修饰单参数构造函数,用于禁止隐式类型转换。
当一个构造函数被声明为 explicit 时,它指示编译器在使用该构造函数进行类型转换时只能使用显式调用,而不允许隐式的类型转换发生。
通过使用 explicit 关键字,可以防止一些意外的类型转换,提高代码的清晰性和安全性。它通常用于防止不必要的类型转换,特别是在单参数构造函数可能引起歧义或产生意外结果的情况下。
preprocessor directive¶
#include_next
的作用是 在寻找头文件时的头文件搜索优先级里,去除该文件所在的当前目录,主要是为C++头文件的重名问题提供一种解决方案。- 正确的用法:代码
b.cpp
想使用 自己拓展修改的stdlib.h
, 那么在代码的目录下创建stdlib.h
,并在该文件里#include_next "stdlib.h"
防止递归引用。
- 正确的用法:代码
define、 const、 typedef、 inline¶
- define:
- define是一个预处理器指令,用于创建宏定义。它在编译之前对源代码进行简单的文本替换。可以用来定义常量、函数宏和条件编译等。
- 优势:灵活性上占优,特别是在需要获取文件名、行号或控制编译(不同平台编译)时行为的场景
- 缺点:宏的调试和排错难度相对较高,因为宏的展开发生在编译前,出错时通常不容易直接定位到宏展开的具体代码。相比函数,宏的类型检查不严格,容易导致隐含的错误。
- 例如:
#define PI 3.14159
,在代码中将PI替换为3.14159。
#
是 字符串化操作符(Stringizing operator)
在 C/C++ 宏中,#
是 字符串化操作符(Stringizing operator),它的作用是将宏参数转换为字符串文字(string literal
)。
当在宏中使用 #key
时,key
被转换为一个字符串文字,即在代码中实际变为 "key"
(包括引号)。如果不加 #
,key
将直接作为标记被使用,不会转为字符串。
示例:
以下是一个宏的例子,演示了 #
的作用:
宏定义:
#define TO_STRING(x) #x
#define CONCAT_AND_PRINT(a, b) printf("Concatenation: %s\n", TO_STRING(a##b));
宏使用:
int main() {
int HelloWorld = 42;
// 使用字符串化操作
printf("%s\n", TO_STRING(HelloWorld)); // 输出: HelloWorld
// 使用标记粘贴操作和字符串化操作
CONCAT_AND_PRINT(Hello, World); // 输出: Concatenation: HelloWorld
}
宏展开与作用:
-
输出:TO_STRING(HelloWorld)
宏TO_STRING
将参数HelloWorld
转换为字符串文字,展开为:
HelloWorld
-
CONCAT_AND_PRINT(Hello, World)
a##b
将Hello
和World
拼接成HelloWorld
。TO_STRING(a##b)
将拼接后的标识符HelloWorld
转换为字符串"HelloWorld"
。 展开为:
输出:Concatenation: HelloWorld
##
是 C/C++ 宏预处理器的 标记粘贴操作符(Token-pasting operator)
操作符将 valueName 粘贴到 aaa 和 bbb 之间,形成新的标识符。例如,使用 aaa##valueName##bbb 这样的语法是完全有效的。¶
用于将宏参数和宏内的其他标记连接起来。具体来说,##Value
和 ##Initialized
这两个标记会与宏参数(如 valueName
)进行拼接,形成新的标识符。
#define REGISTER_OPTION_CACHE(type, valueName, ...) \
static thread_local type valueName##Value; \
static thread_local bool valueName##Initialized = false; \
inline type GetWithCache##valueName() { \
if (!valueName##Initialized) { \
valueName##Value = __VA_ARGS__(); \
valueName##Initialized = true; \
} \
return valueName##Value; \
} \
inline void SetWithCache##valueName(type value) { \
valueName##Value = value; \
valueName##Initialized = true; \
}
宏展开解释
假设你在代码中使用了如下调用:
这将会将宏中的 type
替换为 int
,valueName
替换为 MyValue
,并且 __VA_ARGS__
代表了传递给宏的可变参数 42
。
展开后的代码:
static thread_local int MyValueValue;
static thread_local bool MyValueInitialized = false;
inline int GetWithCacheMyValue() {
if (!MyValueInitialized) {
MyValueValue = 42; // 使用了 __VA_ARGS__
MyValueInitialized = true;
}
return MyValueValue;
}
inline void SetWithCacheMyValue(int value) {
MyValueValue = value;
MyValueInitialized = true;
}
关键点解释:
valueName##Value
被展开为MyValueValue
。##
操作符将宏参数MyValue
与Value
连接,形成新的标识符MyValueValue
。valueName##Initialized
被展开为MyValueInitialized
,同样是将宏参数MyValue
与Initialized
连接,形成新的标识符MyValueInitialized
。
结果
MyValueValue
存储了缓存的值(在这个例子中是42
)。MyValueInitialized
是一个布尔值,用来标记缓存是否已经初始化。GetWithCacheMyValue()
函数首先检查MyValueInitialized
是否为true
,如果没有被初始化,它会使用42
来初始化缓存并设置标志。SetWithCacheMyValue(int value)
函数允许你更新缓存的值,并将MyValueInitialized
设置为true
。
- const:
- const用于声明一个常量,指示标识符的值在程序执行期间不能被修改。
- const可以用于变量、函数参数、函数返回类型和成员函数。使用const可以提高代码的可读性和安全性。
- 例如:
const int MAX_VALUE = 100;
,声明一个名为MAX_VALUE的常量。
- typedef:
- typedef用于为数据类型创建别名。它可以用于为复杂的数据类型提供更简洁的名称,增强代码的可读性和可维护性。
- typedef创建的别名可以像原始类型一样使用,并且不会引入新的类型,只是为已有类型提供了一个新的名称。
- 例如:
typedef int Age
;,为int类型创建了一个别名Age。
- inline:
- inline用于声明内联函数,它是一种编译器的建议,用于将函数的定义直接插入到调用处,以避免函数调用的开销。
- 内联函数通常在函数体较小且频繁调用的情况下使用,可以提高程序的执行效率。
- inline关键字只是给编译器一个提示,编译器可以选择忽略该提示。在大多数情况下,编译器会自动进行内联优化。
-
例如:
inline int add(int a, int b) { return a + b; }
,声明了一个内联函数add。 -
define主要用于宏定义,const用于声明常量,typedef用于创建类型别名,inline用于内联函数的声明。
#ifndef & #pragma once¶
为了避免同一个文件被include多次,C/C++中有两种方式,一种是#ifndef方式,一种是#pragma once方式。在能够支持这两种方式的编译器上,二者并没有太大的区别,但是两者仍然还是有一些细微的区别。
new & delete¶
- new和delete 相对于 malloc/free 分配和释放堆空间。
- 额外会执行构造函数和析构函数
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "Constructing MyClass" << std::endl;
}
~MyClass() {
std::cout << "Destructing MyClass" << std::endl;
}
};
int main() {
// 使用new动态分配内存,并调用构造函数
MyClass* obj = new MyClass();
// 执行一些操作...
// 使用delete释放内存,并调用析构函数
delete obj;
return 0;
}
namespace¶
namespace 会影响 typedef
的作用范围,但不会直接限制 #define
宏的作用范围。
头文件¶
- 相互引用,前置声明
- include头文件其实就是将对应的头文件内容贴在include的位置
A.h, B.h 都需要string.h的头文件,然后B.h 会include A.h,那么我在B.h里是不是可以省略include string.h
不应该省略,
- 防止代码变更引发的问题: 如果某天 A.h 中移除了 #include
,而 B.h 依赖 A.h 提供的 #include ,那么 B.h 将会因找不到 std::string 而编译失败。因此,显式包含依赖的头文件可以避免这种隐含依赖引发的问题。 - 提高可读性和自包含性: 每个头文件应该尽量做到自包含,意思是每个头文件应该独立地包含所有它所需要的头文件。这样做的好处是,任何其他文件都可以安全地单独包含 B.h,而无需额外关心它依赖于哪些头文件。
- 减少隐式依赖: 隐式依赖(依赖另一个头文件帮你包含所需的头文件)可能导致维护性问题。显式 #include 可以让代码更具可预测性和可维护性。
include的位置有什么规则和规律吗,头文件和cpp文件前都可以吗?
在编写代码时,往往A.cpp需要include A.h。那A.cpp需要的头文件,我是写在A.cpp里还是A.h里?
- 头文件 (A.h):只包含声明所需的头文件,不包含仅在实现中需要的头文件。
- 源文件 (A.cpp):包含所有实现需要的头文件,特别是那些仅在实现部分用到的头文件。此外,A.cpp 应该总是包含 A.h。6.
函数的特殊写法¶
函数传参¶
- 值传递
- 引用传递
- 指针传递
//值传递
change1(n);
void change1(int n){
n++;
}
//引用传递,操作地址就是实参地址 ,只是相当于实参的一个别名,在符号表里对应是同一个地址。对它的操作就是对实参的操作
change2(n);
void change2(int &n){
n++;
}
//特殊对vector
void change2(vector<int> &n)
//特殊对数组
void change2(int (&n)[1000])
//指针传递,其实是地址的值传递
change3(&n);
void change3(int *n){
*n=*n+1;
}
引用传递和指针传递的区别:
- 引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
- 不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)。
- 一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)。
指针传递和引用传递的使用情景:
- 函数内部修改参数并且希望改动影响调用者。
- 当一个函数实际需要返回多个值,而只能显式返回一个值时,可以将另外需要返回的变量以指针/引用传递
闭包、匿名函数¶
闭包是捕获并持有了外部作用域变量的函数。
闭包(Closure)是指在程序中,函数可以捕捉并记住其作用域(环境)中的变量,即使在函数执行完成后,这些变量依然保存在内存中,并能在后续的函数调用中被使用。闭包的一个重要特性是,它不仅保存了函数本身的逻辑,还“闭合”了函数执行时的上下文环境(即该函数所在的作用域)。
闭包通常用于实现函数内部的状态保持、回调函数等场景。在 C++ 中,闭包通过 lambda 表达式 实现,lambda 表达式可以捕获外部变量并在其内部使用。
例子
auto add = [](int x) {
return [x](int y) {
return x + y;
};
};
auto add5 = add(5);
std::cout << add5(3); // 输出 8
add
函数返回了一个闭包,捕获了变量 x
的值。即使 x
在原作用域中不再可用,返回的闭包仍然可以访问并使用 x
的值。
- 匿名函数是一种没有被绑定标识符的函数
- lambda 是一种匿名函数
- lambda 可以表示闭包
匿名函数(lambda)和闭包的关系就如同类和类对象的关系
匿名函数和类的定义都只存在于源码(代码段)中,而闭包和类对象则是在运行时占用内存空间的实体;
- Lambda在C++中的实现方式
传参默认值¶
虽然理论上可以通过类似void f(bool x = true)
来实现默认值。但是我在PTA和CPPRINTER上编译.so不过, 会出现undefied的符号。实际实现如下
// hpp
void SetDeterministic();
void SetDeterministic(bool isOpapi = true);
//cpp
void SetDeterministic() {
SetDeterministic(true);
}
void SetDeterministic(bool isOpapi)
{
//xxx
}
类型变参模板¶
条件模板类
假设我们有一个模板类 Wrapper
,我们希望禁止 VirtualGuardImpl
类型作为模板参数:
template <
typename T,
typename U = T,
typename = typename std::enable_if<!std::is_same<U, VirtualGuardImpl>::value>::type>
class Wrapper {
public:
void function() {
// 实现
}
};
在这个例子中,如果用户尝试创建 Wrapper<VirtualGuardImpl>
或 Wrapper<VirtualGuardImpl, VirtualGuardImpl>
的实例,编译器将报错,因为 std::enable_if
的条件不满足。但如果使用其他类型,比如 int
或自定义类型,就可以正常编译。
这段代码是 C++ 中的一个模板函数或模板类模板参数的定义,它使用了模板默认参数、std::enable_if
条件编译技术以及类型萃取(type traits)。下面是对这段代码的详细解释:
-
模板参数
U
:typename U = T
定义了一个模板类型参数U
,并给它一个默认值T
。这意味着如果在使用模板时没有指定U
的话,它将默认使用模板参数T
的值。
-
std::enable_if
:std::enable_if
是一个条件编译技术,它只在给定的布尔表达式为true
时启用某个模板。- 在这个例子中,
std::enable_if
后面的布尔表达式是!std::is_same<U, VirtualGuardImpl>::value
。这意味着只有当U
不等于VirtualGuardImpl
类型时,这个模板参数才有效。
-
typename
关键字:typename
关键字用于告诉编译器std::enable_if
的结果是一个类型。std::enable_if
返回的是一个类型,如果条件为true
,它返回一个空的类型,否则会导致编译错误。
-
std::is_same
:std::is_same<U, VirtualGuardImpl>::value
是一个编译时检查,用于判断U
和VirtualGuardImpl
是否是相同的类型。::value
是类型特征std::is_same
的一个成员,它是一个布尔值,如果类型相同则为true
,否则为false
。
-
组合解释:
- 这段代码的意思是:定义一个模板参数
U
,默认值为T
,并且这个模板参数只有在U
不是VirtualGuardImpl
类型时才有效。 - 这是一种常见的模板编程技巧,用于约束模板参数的类型,以确保它们符合特定的要求。
- 这段代码的意思是:定义一个模板参数
个数变参模板¶
#include <stdarg.h>
void Error(const char* format, ...)
{
va_list argptr;
va_start(argptr, format);
vfprintf(stderr, format, argptr);
va_end(argptr);
}
VA_LIST 是在C语言中解决变参问题的一组宏,变参问题是指参数的个数不定,可以是传入一个参数也可以是多个;可变参数中的每个参数的类型可以不同,也可以相同;可变参数的每个参数并没有实际的名称与之相对应,用起来是很灵活。
(1)首先在函数里定义一具VA_LIST型的变量,这个变量是指向参数的指针; (2)然后用VA_START宏初始化变量刚定义的VA_LIST变量; (3)然后用VA_ARG返回可变的参数,VA_ARG的第二个参数是你要返回的参数的类型(如果函数有多个可变参数的,依次调用VA_ARG获取各个参数); (4)最后用VA_END宏结束可变参数的获取。
系统提供了vprintf系列格式化字符串的函数,用于编程人员封装自己的I/O函数。
int vprintf / vscanf (const char * format, va_list ap); // 从标准输入/输出格式化字符串
int vfprintf / vfsacanf (FILE * stream, const char * format, va_list ap); // 从文件流
int vsprintf / vsscanf (char * s, const char * format, va_list ap); // 从字符串
返回多个数¶
使用结构体
struct RowAndCol { int row;int col; };
RowAndCol r(string fn) {
/*...*/
RowAndCol result;
result.row = x;
result.col = y;
return result;
}
左值与右值¶
在 C++ 中,左值(lvalue) 和 右值(rvalue) 是两个重要的概念,用来描述表达式的值和内存的关系。它们帮助开发者理解变量的生命周期、赋值和对象管理,特别是在现代 C++ 中引入了右值引用后,优化了移动语义和资源管理。
1. 左值(lvalue)¶
左值(lvalue,locatable value) 是指在内存中有明确地址、可持久存在的对象,可以对其进行赋值操作。通俗地说,左值是能够取地址的值,可以出现在赋值操作符的左边。
特点:
- 左值具有持久的内存地址。
- 左值可以取地址(使用
&
运算符)。 - 左值通常表示已经存在的变量或对象。
示例
在这个例子中,x
是一个左值,因为它表示了内存中的某个对象,并且可以通过赋值语句修改它的值。
2. 右值(rvalue)¶
右值(rvalue,readable value) 是没有明确地址、临时存在的对象,不能对其进行赋值操作。它们通常是字面值常量或表达式的结果。右值只能出现在赋值操作符的右边,表示一个临时对象或数据。
特点:
- 右值是临时的,通常会在表达式结束时销毁。
- 右值不能取地址(即不能使用
&
获取右值的地址)。 - 右值表示表达式的计算结果或临时对象。
示例
在这个例子中,10
和 y + 5
是右值,因为它们表示计算出的临时数据,并且不能直接对这些值进行赋值操作。
3. 现代 C++ 中的右值引用(rvalue reference)¶
C++11 引入了 右值引用,即通过 &&
符号表示。这使得右值也能通过引用进行操作,特别是在实现移动语义(move semantics)和避免不必要的拷贝时非常有用。右值引用允许我们通过右值管理资源,避免性能上的损失。
示例:右值引用与移动语义
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec1 = {1, 2, 3};
std::vector<int> vec2 = std::move(vec1); // vec1 资源移动到 vec2
std::cout << "vec1 size: " << vec1.size() << std::endl;
std::cout << "vec2 size: " << vec2.size() << std::endl;
return 0;
}
在这个例子中,std::move
将 vec1
变为一个右值引用,使其内部的资源(如动态分配的内存)直接转移给 vec2
,避免了拷贝。
4. 区分左值与右值¶
通常,左值是表示持久存在的对象,可以通过取地址符 &
获取其地址,而右值是临时的、短暂存在的值,不能直接获取其地址。理解这两者对于编写高效的 C++ 代码和使用现代特性(如右值引用和移动语义)非常重要。
常见误区
- 字面值常量(如 42、'a')是右值。
- 表达式的结果(如
x + y
)通常是右值。 - 函数返回值若返回的是值,而不是引用,则该返回值是右值。
- 左值(lvalue) 是可以取地址的值,通常是变量或持久的对象。
- 右值(rvalue) 是临时值,通常是表达式的结果或字面量。
- 右值引用(
&&
)是 C++11 引入的新特性,用来优化资源管理和避免不必要的拷贝操作。
C++11: 花括号初始化列表¶
使用¶
在C++98/03中我们只能对普通数组和POD(plain old data,简单来说就是可以用memcpy复制的对象)类型可以使用列表初始化,如下:
在C++11中初始化列表被适用性被放大,可以作用于任何类型对象的初始化。如下:
X x1 = X{1,2};
X x2 = {1,2}; // 此处的'='可有可⽆
X x3{1,2};
X* p = new X{1,2};
//列表初始化也可以用在函数的返回值上
std::vector<int> func() {
return {};
}
变量类型的适用范围¶
聚合类型可以进行直接列表初始化
聚合类型包括
- 普通数组,如int[5],char[],double[]等
- 一个类,且满足以下条件:
- 没有用户声明的构造函数
- 没有用户提供的构造函数(允许显示预置或弃置的构造函数)
- 没有私有或保护的非静态数据成员
- 没有基类
- 没有虚函数
- 没有{}和=直接初始化的非静态数据成员
- 没有默认成员初始化器
原理¶
对于一个聚合类型,使用列表初始化相当于使用std::initializer_list
对其中的相同类型T
的每个元素分别赋值处理,类似下面示例代码;
struct CustomVec {
std::vector<int> data;
CustomVec(std::initializer_list<int> list) {
for (auto iter = list.begin(); iter != list.end(); ++iter) {
data.push_back(*iter);
}
}
};
优势¶
- 方便,且基本上可以替代括号初始化
- 可以使用初始化列表接受任意长度
- 可以防止类型窄化,避免精度丢失的隐式类型转换
参考文献¶
-
⼩贺 C++ ⼋股⽂ PDF 的作者,电⼦书的内容整理于公众号「herongwei」
https://shaojiemike.notion.site/C-11-a94be53ca5a94d34b8c6972339e7538a