类型
int小于等于数据线宽度,size_t大于等于地址线宽度。
size_t
通常将一些无符号的整形定义为size_t,比如说unsigned int或者unsigned long,甚至unsigned long long。\
sizeof的返回值类型都是size_t
在x64下,size_t的大小是8个字节,在x86下,size_t的大小是4个字节。
size_type是STL中类似size_t的类型,一般情况下size_type和size_t的大小相等。
由string类类型和vector类类型定义的类型,用以保存任意string对象或vector对象的长度。
vector.size()的返回值类型是size_type
常量类型
auto x1 = 0b101010; // 二进制,C++14开始的特性
auto x1 = 04; // 八进制
auto x1 = 0xfee; // 十六进制
auto x1 = 4u; // unsigned int
auto x1 = 4ll; // long long int
auto x2 = 4ull; // unsigned long long
auto x3 = 4.0; // double
auto x4 = 4.0f; // float
auto x5 = 4l; // long double,根据编译器,至少拥有和double相同的精度和范围
自增运算符与函数传参
函数参数压栈的顺序是从右到左。
1.为了与内置类型一致,前缀式操作符应返回被增量或减量对象的引用
2.为了与内置类型一直,后缀式操作符应返回旧值(即,尚未自增或自减的值),并且,应作为值返回,而不是返回引用。
int k = 1;
printf("%d,%d\n",k++,k++);
// 输出2, 1
int k = 1;
printf("%d,%d\n",++k,++k);
// 输出3, 3
数组和指针
char *s = "hello world"
声明了一个rodata段的全局常量字符串,s作为一个栈上的指针变量指向这个字符串。
“hello world”在表达式中就代表字符串的地址,可以用printf("%d\n", "Hello World");
打印出字符串的地址。char s[] = "hello world"
声明了一个栈上的局部数组变量,这种方式声明的得到的s不占用单独的内存,但依然有s=&s=&s[0]
(在语法上s既可以看作整个数组,也可以看作数组开头的内存地址)
二维数组
如果数组长度固定,二维数组不会带来额外的开销。
如果数组长度不固定,尽量用一维数组代替二维数组。
如果不是矩形数组(二级数组的长度不一样),只能用二级指针的方式new二维数组。
int **array;
array = new int *[10];
for(int i = 0; i < 10; i++)
{
array[i] = new int [5];
memset(array[i], 0, 5 * sizeof(int));
}
for(int i = 0; i < 10; i++)
{
delete[] array[i];
}
delete[] array;
枚举enum
未限域枚举(unscoped enum)
enum Color { black, white, red }; //black, white, red在Color所在的作用域
auto white = false; //错误! white早已在这个作用域中声明
使用限域enum来减少命名空间污染,这是一个足够合理使用它而不是它的同胞未限域enum的理由,其实限域enum还有第二个吸引人的优点:在它的作用域中,枚举名是强类型。未限域enum中的枚举名会隐式转换为整型(现在,也可以转换为浮点类型)。
限域枚举(scoped enum)
因为scoped enum是通过“enum class”声明,所以它们有时候也被称为枚举类(enum classes)。
不存在任何隐式转换可以将限域enum中的枚举名转化为任何其他类型。
enum class Color { black, white, red }; //black, white, red限制在Color域内
auto white = false; //没问题,域内没有其他“white”
Color c = white; //错误,域中没有枚举名叫white
Color c = Color::white; //没问题
auto c = Color::white; //也没问题(也符合Item5的建议)
指定枚举名占用多少字节
限域enum底层类型默认是int。非限域enum没有默认底层类型。
enum Color: std::uint8_t; //非限域enum前向声明,底层类型为std::uint8_t
enum class Status: std::uint32_t { good = 0,
failed = 1,
incomplete = 100,
corrupt = 200,
audited = 500,
indeterminate = 0xFFFFFFFF
};
using, typedef
定义别名时没有区别,using可以定义模板别名,typedef不行。using常用语模板中操纵类型别名。
using可以声明命名空间。
typedef int p()
// 定义了一个名为p的函数,其返回值为int,不需要参数
using p = int()
typedef int func_t_t(int, int);
// 定义了一个名为func_t_t的函数类型,其返回值为int,参数为(int,int)
typedef int(*func_t_p)(int, int);
//函数指针
typedef int(&func_t_r)(int, int);
//函数引用
using func_u_t = int(int, int);
using func_u_p = int(*)(int, int);
using func_u_r = int(&)(int, int);
在模板中
template<typename T>
struct Echo
{
using Type = T;
};
预处理,宏
extern “c”
extern “C”包含双重含义,从字面上可以知道,首先,被它修饰的目标是”extern”的;其次,被它修饰的目标代码是”C”的。
由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。如果 C++程序要调用已经被编译后的C 函数,就可以用extern “C”来声明这是一个C语言函数
在C++中使用C语言
#ifdef NULL
#define NULL 0
#endif
#ifdef DEBUG
cerr <<"Variable x = " << x << endl;
#endif
//等效于注释
#ifdef DEBUG
cerr <<"Variable x = " << x << endl;
#endif
C++预定义宏
C++宏 | 描述 |
---|---|
LINE | 这会在程序编译时包含当前行号。 |
FILE | 这会在程序编译时包含当前文件名。 |
DATE | 这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。 |
TIME | 这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。 |
// 把MKSTR(xxx)转换成字符串"xxx"
#define MKSTR( x ) #x
// CONCAT(a,b)会合成一个变量名ab
#define CONCAT( x, y ) x ## y
位运算
~是取反,求补码
优先级:~ > ! > & > ^ > | > && > ||
注意:~和!,&和^,它们之间的优先级不是完全确定的,写的时候最好带括号
运算符优先级
https://docs.microsoft.com/zh-cn/cpp/cpp/cpp-built-in-operators-precedence-and-associativity?view=msvc-160
switch
语句,只要符合一个case的条件,就会执行后面所有的case跟的语句,包括default语句
noexcept
参考:https://www.cnblogs.com/sword03/p/10020344.html
noexcept指定符
C++中的异常处理是在运行时而不是编译时检测的。为了实现运行时检测,编译器创建额外的代码,然而这会妨碍程序优化。noexcept关键字告诉编译器,函数中不会发生异常,这有利于编译器对程序做更多的优化。在以往的C++版本中常用throw()表示,在C++ 11中已经被noexcept代替。如果在运行时,noexecpt函数向外抛出了异常(如果函数内部捕捉了异常并完成处理,这种情况不算抛出异常),程序会直接终止,调用std::terminate()函数,该函数内部会调用std::abort()终止程序。
有条件的noexcecpt
这个例子表示,如果操作x.swap(y)不发生异常,那么函数swap(Type& x, Type& y)一定不发生异常。
void swap(Type& x, Type& y) noexcept(noexcept(x.swap(y))) //C++11
{
x.swap(y);
}
noexcept运算符
noexcept运算符进行编译时检查,若表达式声明为不抛出任何异常则返回true。
用于函数模板
参考:https://blog.csdn.net/luoshabugui/article/details/86255100
noexcept 指定符和noexcept 运算符配合用于函数模板,以声明函数将对某些类型抛出异常,但不对其他类型抛出。
template <class T>
void fun() noexcept(noexcept(T()))
{
.....
}
std::begin, std::end
获取数组内存地址
int arr[] = { 1,5,9,10,9,2 };
cout << end(arr) << endl;
cout << begin(arr) << endl;
//end(arr) - begin(arr)就是数组的长度,不用除以sizeof(int)
auto x = end(arr) - begin(arr);
//cout << typeid(x).name() << endl;
cout << x << endl;
lambda表达式
[] (int x, int y) { return x + y; } // 隐式返回类型
[] (int x, int y) -> int { return x + y; } // 指明返回类型
[] (int& x) { ++x; } // 没有 return 语句 -> Lambda 函数的返回类型是 'void'
[] () { ++global_x; } // 没有参数,仅访问某个全局变量
[] { ++global_x; } // 与上一个相同,省略了 (操作符重载函数参数)
[x](int a) mutable { x *= 2; return a + x; };
// 复制捕捉x,mutable关键字使x可以在lambda函数内被修改,但是不影响外面的x
多层labmda嵌套(函数式编程)
[]:默认不捕获任何变量;
[=]:默认以值捕获所有变量;
[&]:默认以引用捕获所有变量;
[x]:仅以值捕获x,其它变量不捕获;
[&x]:仅以引用捕获x,其它变量不捕获;
[=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;
[&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
[this]:通过引用捕获当前对象(其实是复制指针);
[*this]:通过传值方式捕获当前对象;
mutable匿名函数按值捕获的变量行为类似于static变量,每次调用lambda函数都会访问同一个变量。(在Python中定义默认参数为可变参数时也一样)
捕获发生在匿名函数声明的时候,而不是调用的时候。
指针加减法
指向数组元素指针加1就意味着移动到下一个元素,不用加sizeof(int)
对于数组a[5], a和&a的地址是一样的,但意思不一样:
a是数组首地址,也就是a[0]的地址,a+1是数组下一元素的地址,即a[1],
&a是对象(整个数组作为一个对象)首地址,而&a+1是下一个对象的地址,即a[5].
运算符重载
类的成员函数重载(类里边)
Box operator+(const Box&);
非成员函数重载(类外边)
Box operator+(const Box&, const Box&);
异常处理
类型转换
强制类型转换(括号运算符)
括号运算符可以完成所有的转换。由于编译器可能会尝试一些意料之外的方式进行转换,应尽量避免使用。
隐式类型转换
传参时自动发生
无符号整数和有符号整数比大小会隐式转换成无符号整数!!!
注意:STL容器.size()会返回无符号类型!!
static_cast(expr)
static_cast 是“静态转换”的意思,也就是在编译期间转换,转换失败的话会抛出一个编译错误。任何编写程序时能够明确的类型转换都可以使用static_cast。不提供运行时的检查。
主要在以下几种场合中使用:
用于类层次结构中,父类和子类之间指针和引用的转换;进行上行转换,把子类对象的指针/引用转换为父类指针/引用,这种转换是安全的;进行下行转换,把父类对象的指针/引用转换成子类指针/引用,这种转换是不安全的,需要编写程序时来确认;
用于基本数据类型之间的转换,例如把int转char,int转enum等,需要编写程序时来确认安全性;
把void指针转换成目标类型的指针(这是极其不安全的);
static_cast 只能用于良性转换,这样的转换风险较低,一般不会发生什么意外,例如:
- 原有的自动类型转换,例如int转换成char,把int转换成enum、short 转 int、int 转 double、const 转非 const、向上转型等;
- void 指针和具体类型指针之间的转换,例如void 转int 、char 转void 等;
- 有转换构造函数或者类型转换函数的类与其它类型之间的转换,例如 double 转Complex(调用转换构造函数)、Complex 转 double(调用类型转换函数)。
需要注意的是,static_cast 不能用于无关类型之间的转换,因为这些转换都是有风险的,会抛出编译错误。
dynamic_cast(expr) 基础类型转换
只能转换指针类型和引用类型,其它类型(int、double、数组、类、结构体等)都不行。
基类必须包含虚函数,因为运行时类型检查需要运行时类型信息,而这个信息存储在类的虚函数表中。
对于指针,如果转换失败将返回 NULL;对于引用,如果转换失败将抛出std::bad_cast异常。
dynamic_cast 与 static_cast 是相对的,dynamic_cast 是“动态转换”的意思,static_cast 是“静态转换”的意思。
1. 向上转型(Upcasting)和向下转型(Downcasting)
向上转换和向下指转换的是转换指针类型,指向的对象不变,只能用更上面(基类)的指针指向当前对象。
向上转型时,只要待转换的两个类型之间存在继承关系,并且基类包含了虚函数(这些信息在编译期间就能确定),就一定能转换成功。因为向上转型始终是安全的,所以 dynamic_cast 不会进行任何运行期间的检查,这个时候的 dynamic_cast 和 static_cast 没有区别。
向下转型是有风险的,dynamic_cast 会借助 RTTI 信息进行检测,确定安全的才能转换成功,否则就转换失败。
向下转型实质上也是向上转型。只是因为基类指针指向了继承链(Inheritance Chain)更下边的类,所以从类型上看是向下转型。
继承链:每个类都会在内存中保存一份类型信息,编译器会将存在继承关系的类的类型信息使用指针“连接”起来,从而形成一个继承链(Inheritance Chain),也就是如下图所示的样子:
2. void*的转换
将一个指针转换成void*
A *pA = new A;
void *pV = dynamic_cast<void *>(pA);
3. 菱形继承中的转换
菱形继承的转换需要指定转换路径,下面的例子中如果将D直接转换成A会返回NULL
class A { virtual void f() {}; };
class B :public A { void f() {}; };
class C :public A { void f() {}; };
class D :public B, public C { void f() {}; };
void main()
{
D *pD = new D;
B *pB = dynamic_cast<B *>(pD);
A *pA = dynamic_cast<A *>(pB);
A *pA = dynamic_cast<A *>(pD); // pA = NULL
}
reinterpret_cast(expr)
reinterpret 是“重新解释”的意思,顾名思义,reinterpret_cast 这种转换仅仅是对二进制位的重新解释,不会借助已有的转换规则对数据进行调整,非常简单粗暴,所以风险很高。
可以取右值的地址。
int *p = reinterpret_cast<int*>(100);
A *p = reinterpret_cast<int*>(new A(25, 96));
cosnt_cast(expr)
待转换必须为指针或引用。
用于移除类型的const、volatile和__unaligned属性
常量指针/引用被转化成非常量指针/引用,volatile类型转换成非volatile类型,并且仍然指向原来的对象。
如果还用原来的变量标识符去访问,编译器仍然会输出原来的常量值。因为 C++ 对常量的处理更像是编译时期的#define,是一个值替换的过程。
int main(void)
{
const int b = 10;
//b = 11 报错,因为b是一个常量对象
int * pc = const_cast<int *>(&b);
*pc = 11;
cout << "*pc = " << *pc << endl;//b原来地址的数据现在可由*pc来改变,即解除const
cout << "b = " << b << endl; //b其实类似(编译器处理问题)#define b 10 不会变的
cout << "*&b = " << *&b << endl;
return 0;
}
注意:只能用这种方式去改变局部const变量的值,const全局变量存放在rodata段,试图用这种方式操作rodata段的数据会引起段错误。
智能指针
std::shared_ptr
允许多个智能指针指向同一个对象,内部有引用计数机制。
通过 get() 方法来获取原始指针。
通过use_count()来查看一个对象的引用计数。
通过 reset() 来重置指针,并减少一个引用计数。
auto pointer = std::make_shared<int>(10);
auto pointer2 = pointer; // 引用计数+1
auto pointer3 = pointer; // 引用计数+1
int *p = pointer.get(); // 这样不会增加引用计数
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 3
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 3
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 3
pointer2.reset();
std::cout << "reset pointer2:" << std::endl;
std::cout << "pointer.use_count() = " << pointer.use_count() << std::endl; // 2
std::cout << "pointer2.use_count() = " << pointer2.use_count() << std::endl; // 0, pointer2 已 reset
std::cout << "pointer3.use_count() = " << pointer3.use_count() << std::endl; // 2
std::unique_ptr
禁止其他智能指针与其共享同一个对象。
可以利用 std::move 将其转移给其他的 unique_ptr。
std::unique_ptr<int> pointer = std::make_unique<int>(10); // make_unique 从 C++14 引入
std::unique_ptr<int> pointer2 = pointer; // 非法
std::weak_ptr
memory fence
变量修饰符
整数长度
short int // 2 bytes
int // 4 bytes
long int // 8 bytes
const
全局const变量存储在BSS段(未初始化)或rodata段(已初始化)。
局部const变量存储在栈上。
编译器会对const变量的标识符做类似于#define var_name xxx的处理,所以即使储存的值变了,用原来的标识符访问变量得到的结果还是原来的结果。
修改全局const变量编译时会报错。
修饰类的成员函数,放在形参后面,大括号前面。const成员函数不可修改类中成员变量的值。
int operator+(int a) const {return this.xx + a}
const修饰指针
参考:https://www.zhihu.com/question/443195492/answer/1723886545
不管const写成如何,读懂别人写的const和*满天飞的类型的金科玉律是const默认作用于其左边的东西,否则作用于其右边的东西。
constexpr
effective modern c++条款15:尽可能多地使用constexpr
当用于对象上面,它本质上就是const的加强形式,constexpr表明一个值不仅仅是常量,还是编译期可知的,而const对象不需要在编译期初始化它的值。
当它用于函数上,constexpr意思就大不相同了。constexpr表示在编译阶段就尽可能执行函数计算返回值,返回的变量存入常量段。
- constexpr函数可以用于需求编译期常量的上下文。如果你传给constexpr函数的实参在编译期可知,那么结果将在编译期计算。如果实参的值在编译期不知道,你的代码就会被拒绝。
当一个constexpr函数被一个或者多个编译期不可知值调用时,它就像普通函数一样,运行时计算它的结果。这意味着你不需要两个函数,一个用于编译期计算,一个用于运行时计算。constexpr全做了。
constexpr //pow是绝不抛异常的 int pow(int base, int exp) noexcept //constexpr函数 { … //实现在下面 } constexpr auto numConds = 5; //(上面例子中)条件的个数 std::array<int, pow(3, numConds)> results; //结果有3^numConds个元素
C++11中,constexpr函数的代码不超过一行语句:一个return。听起来很受限,但实际上有两个技巧可以扩展constexpr函数的表达能力。第一,使用三元运算符“?:”来代替if-else语句,第二,使用递归代替循环。因此pow可以像这样实现:
constexpr int pow(int base, int exp) noexcept { return (exp == 0 ? 1 : base * pow(base, exp - 1)); }
这样没问题,但是很难想象除了使用函数式语言的程序员外会觉得这样硬核的编程方式更好。在C++14中,constexpr函数的限制变得非常宽松了,所以下面的函数实现成为了可能:
constexpr int pow(int base, int exp) noexcept //C++14 { auto result = 1; for (int i = 0; i < exp; ++i) result *= base; return result; }
restrict
由 restrict 修饰的指针是唯一一种访问它所指向的对象的方式。
volatile
volatile是告诉编译器我们正在处理特殊内存。意味着告诉编译器“不要对这块内存执行任何优化”。
1.阻止编译器为了提高速度将一个变量缓存到寄存器内而不写回。
修饰符 volatile 告诉编译器不需要优化volatile声明的变量,让程序可以直接从内存中读取变量。对于一般的变量编译器会对变量进行优化,将内存中的变量值放在寄存器中以加快读写效率。
2.阻止编译器调整操作volatile变量的指令顺序(不一定,不同编译器实现不同)。可见性和生命周期
static
可作用于全局变量、局部变量、函数、类的函数、类的变量。
主要有两个作用:可见性,存储位置(影响生命周期)
静态全局变量和静态函数只在本文件可见。
静态变量存储在Data段或BSS段。
作用于函数时,该函数只在本文件内使用,所以不能在.h文件中声明static全局函数,如果本文件内该函数没有被使用,优化时可能直接去除该函数。
头文件中只能用static inline。
cpp文件中也不要定义非static的函数,所有提供给其他文件使用的函数都把函数声明加到头文件中去。
register
register 存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。
extern
mutable
mutable 说明符仅适用于类的对象,和const成员函数一起使用,允许const成员函数修改类中mutable修饰的属性。
lambda表达式中mutable代表可以修改捕获列表中的变量。
thread_local
使用 thread_local 说明符声明的变量仅可在它在其上创建的线程上访问。 变量在创建线程时创建,并在销毁线程时销毁。 每个线程都有其自己的变量副本。