变量的大小
变量 | 环境 | 大小 |
---|---|---|
指针 | 32 | 4 |
char | 32 | 1 |
short | 32 | 2 |
int | 32 | 4 |
float | 32 | 4 |
double | 32 | 8 |
long | 32 | 4 |
long long | 32 | 8 |
32 位和 64 位唯一的区别就是指针和 long 变成了 8。两者默认都是 4 字节对齐。
32 位环境中 float 的有效位是 7 位,double 的有效位是 16 位
另外就是 0x7fff_ffff 代表了 int32 的最大值,0x8000_0000 代表了 in32 的最小值。0xffff_ffff 实际上是 -1
枚举
通过 enum
关键字可以定义枚举类型。枚举类型可以声明变量,进行赋值:
enum Shirt {
Small = 1, Medium = 2, Large = 3, Xlarge = 5
};
Shirt shirt = Shirt::Small;
Shirt shirt = Shirt(2)
枚举值的取值范围
假设枚举类型中的最大值为Max,最小值为Min。枚举类型的上限为RightLimit,下限为LeftLimit,则:
RightLimit = \(2^k-1\) , k为使 RightLimit > Max 的最小值
LeftLimit = \(- 2^k+1\) , k为使 LeftLimit < Min 的最大值。当 Min ≥ 0 时,LeftLimit = 0
强类型枚举
强类型枚举 (Scope Enum)
可以通过 enum class
声明。其与普通枚举不同之处在于:
强类型枚举无法被隐式转换为整数
强类型枚举无法与整数比较
不同强类型枚举的枚举值无法进行比较
使用强类型枚举可以指定枚举的底层储存方式。
例如:
enum class Egg : unsigned { Small, Medium, Large, Jumbo }
=== * 强类型枚举也可以通过 enum struct
的方式声明 * 强类型枚举的底层储存方式默认为 int ===
字面量
自定义字面量[1]
通过 operator ""X
可以自定义字面量,X 是你的字面量后缀,必须 以下划线开头。 operator "" 可以有两种重载形式:
形式 | 说明 |
---|---|
|
|
operator ""X(const char*, unsigned long long len) | 匹配字符串 |
重载时有以下内容需要注意:
重载时的 X 的名字必须以下划线开头。
重载的第一个参数必须为
unsigned long long
,unsigned long double
,long double
,char
,const char*
之一。第二个参数必须为unsigned long long
。当第一个参数为
const char*
时,使用第二种重载形式,否则选择第一种重载形式。字面量在匹配
operator ""
时不会发生任何形式的隐式类型转换。
int operator ""_G(long double b){
// 匹配 10.2_G
return 10;
};
int operator ""_G(unsigned long long b){
// 匹配 10_G
return 20;
}
int operator ""_G(const char* s, unsigned long long len){
// 匹配 "10"_G
return 30;
}
=== u8 前缀所代表的 UTF-8 字符串最初在 C++11 中被引入,但是其所代表的确切类型 char8_t 直到 C++17 才出现,但是由于实现不理想, u8 在 C++20 中已经不被鼓励使用。所有 u8 相关的操作在 C++20 中被遭到禁用。 |
内存和对象
对象模型
C++ 程序中包含了对对象的创建、销毁、引用、访问以及操作。对象的创建有三种方式:
通过类型定义创建一个对象
通过 new 表达式创建对象
通过隐式创建一个对象(即当改变 union 的
激活成员 (Active Member)
或创建一个临时对象时 )
一个对象需要在创建后需要占用内存空间、需要有名字、有生命周期、有类型,甚至有多态,根据实现的不同,C++ 得以在运行时或得对象的类型。
如果一个对象被包含在其他对象中,那么就叫它 子对象 (Subobject)
,子对象可以是 成员子对象 、 基类子对象 、 数组元素 。如果一个对象不是任何其他对象的子对象,那么就叫它 完全对象 (Complete Object)
。
Lambda 表达式
Lambda 表达式创建了一个 闭包 :一种匿名函数对象
Lambda 表达式是一个右值
Lambda 是一个可调用对象
lambda 表达式的完整语法定义如下:
[捕获列表](参数列表) mutable(可选) 异常 -> 返回类型{ // 函数体 }
捕获列表有三种方式:
捕获方式 | 效果 |
---|---|
var | 按值捕获对象 |
= | 按值捕获作用于内的所有对象 |
& | 参数按引用捕获 |
this | 类中所有变量对 lambda 可见 |
直接使用符号的话代表捕获当前作用域内的所有对象,否则代表捕获一个变量,多个变量以都好分开:
int a = 10;
auto l1 = [&](){}; //按引用捕获当前作用域内所有可见对象
auto l2 = [&l1, &a]; // 按引用捕获 l1 和 a
Lambda 的返回类型可以省略,在 C++14 中可以使用 auto 自动推断返回类型:
auto add = [](auto x, auto y) -> decltype(x+y){
return x+y;
};
Lambda 作为函数参数
Lambda 作为函数参数引入时有三种方式:
使用模板参数
template <typename T> void show(T obj){ obj(); } int main(){ show([](){ cout<<"hello, world"; }); return 0; }
使用退化的Lambda
using foo = void (int) ; void fun(foo f){ f(1); } int main() { auto s = [](int a){ std::cout<<a<<std::endl; }; fun(s); return 0; }
这种方式要求 Lambda 没有捕捉任何变量且函数指针所指向的函数必须跟 Lambda 表达式有相同的调用方式 使用 functional
#include <functional> void show(std::function<void()> obj){ obj(); } int main(){ show([](){ cout<<"hello, world"; }); return 0; }
在使用 Lambda 捕获局部变量时一定要注意变量的生命周期,尤其是在 Qt 的 connect 中,一旦 Lambda 访问了已经析构的变量,就可能导致程序在未抛出任何有效信息的情况下直接崩溃 |
列表初始化
让我们来看一种比较特殊的语法:
std::string func(){
return {};
}
上述代码意味着 func() 返回一个使用默认构造函数构造的 std::string 。通过在大括号内填充不同的参数,可以调用不同的构造函数。[1]
auto
auto 是 C++ 11 引入的关键字,主要用于省去冗余的类型声明。
auto 不能推断 cv 限定
auto 无法推断引用类型。因此使用 auto& 才能创建左值引用,否则会导致对象拷贝
原地初始化时无法使用 auto
某次给 lua 写模块时,由于使用 auto 导致了第二次调用模块函数时的 core dump。改成 auto& 解决了问题 |
函数
在函数传递参数的时候需要注意实参的顺序,实参是从右向左一次入栈的,因此对于下列代码:[2]
int f(int a, int b, int c){
return 0;
}
int main(){
return f(printf("a"),printf("b"),printf("c"));
}
的执行结果为:
cba
指针
成员函数指针:
通过对指定成员函数指针可以调用基类函数。例如:
[, cpp]
struct A{ virtual int func(){ return 10; } };
struct B: public A{ int func() override{ return 20; } };
int main() { auto p = new B; cout<<p→A::func(); return 0; }
程序运行的结果为:
10
另外是指针和数组的区别
以 int * 方式声明的为指针,sizeof 的结果在 32 位系统下为 4,在 64 位系统下为 8
以 int [] 方式声明的为数组,sizeof 的结果为数组的大小(变量的大小乘以数组的尺寸)
另外,如果函数的参数为 int [],那么它返回的尺寸是指针的大小
将数组类型赋值到指针上,就会丢失数组的信息
二维数组的两个下标 int [r][c] 分别代表行和列
二维数组的创建方式为 new int* [],传递方式为 int** 或者:
void func(int a[4][4]);
行数和列数不能省略
多维数组的最高维只用来判断参数类型,因此 int array[5][6]==int array[][6]
,但是其他维度必须写明大小。并且多维数组在传递后再使用for···auto会报错。
开关位
通过按位与和按位或我们可以实现将变量特定的位打开,例如:
unsigned a = 0xaabaa;
auto b = (a & 0xff0ff) | 0x00a00; // b == 0xaaaaa
可见,要关闭位可以使用 0xffxxff 的形式,要打开位可以使用 0x00x00 的形式。要批量设置某一个范围的内存,需要先将内存清零再设置
指针
指针的声明有以几种方式:
声明方式 | 含义 | 储存的内容 |
---|---|---|
int *p | p 是指针 | int 变量的地址 |
int **p | p 是二级指针 | 指针的地址 |
int p[] | p 是数组 | int 数字 |
int (*p)[] | p 是数组指针 | 指向一个数组的地址 |
int *p[] | p 是指针数组 | 数组 |
int p[4][4] | p 是二维数组 | 包含四个元素的数组 |
另一种是串数组:
#include<stdio.h>
int main(){
static char *s[] = {"black", "white", "pink", "violet"};
char **ptr[] = {s+3, s+2, s+1, s}, ***p;
p = ptr;
++p;
printf("%s", **p+1);
return 0;
}
s + 1 会调到字符串数组的第一个索引的位置 因此数组 ptr 指向的三个储存的三个串分别是 {"pink", "white","black", "black"}
将 ptr 赋值给 p 后,p 就丢失了数组信息,这是对其执行 +1 操作只是对字符操作罢了。因此 ++p 指向的是 ink\0 。打印截止到 \0 为止
野指针是指没有初始化的指针。悬空指针是指值为 nullptr 的指针
sizeof
sizeof 对类型得到的是类型的大小 sizeof 对数组得到的是数组的大小 sizeof 对指针得到的是指针的大小
当数组被传递到函数后,就丢失了它的尺寸信息
const
const 变量必须在声明时进行初始化
const* 是导致指针的内容无法更改
*const 会导致指针的指向无法更改
更一般的,C++ 还引入了 constexpr 用来在编译器对表达式/函数求值
volatile
大小端
两个十六进制数一共是八位[3]
大小端是指数据在内存中储存的形式,寄存器中总是大端的
大端就是书面上常用的形式,小端就是将字节顺序逆序排列。地址从左向右是升高的
大端小端没有谁优谁劣,各自优势便是对方劣势:
小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。
大端模式 :符号位的判定固定为第一个字节,容易判断正负。
一般计算机是小端序的,而通信是大端序的
移位运算
移位运算是在寄存器中的运算,与大小端无关。向左移位总是相当于乘以二的次幂,向右移位总是相当于除以二的次幂
内联函数
内联函数必须放到头文件中,编译器必须能看到内敛函数的定义
inline 函数应该简洁,如果语句较多,不适合定义为内联函数
inline 函数中,一般不建议有循环、if 或 switch 语句,否则,函数定义时即使有 inline 关键字,编译器也可能会把该函数作为非内联函数处理。
inline 函数要在函数被调用之前声明。
当虚函数表现出多态时,inline 不起作用 |
内存对齐
使用 attributepacked
和 attributealigned(0)
可以防止编译器对结构体进行对齐
预编译指令
#define S(x) #x
#define X(x) x
#define v abc
int main() {
cout<<S(X(v));
return 0;
}
结果是:
X(v)
此外 ##
是用来拼接字符串的
#define X(x) x##x
int main() {
cout<<X(123);
return 0;
}
结果是:
123123
C++ 中一些奇怪的类型
引用
char* c=new char('a');
char *&r = c; // r为char*的引用
指针
int *a=new int (1);
int **b=&a; // b为指针的指针
int ***c=&b; // c为三重指针
// 指针只能一重一重的取地址,但是可以多重解引用
类
class A{
public:
int a;
};
int main(int argc,char *argv[]) {
A a;
A*b=new A;
int A::*c=&A::a; // c为成员指针
a.*c;// 栈成员使用成员指针
b->*c;// 堆成员使用成员指针
}
函数
函数也是有类型的,其类型由函数返回值和函数参数唯一确定,下面以vs2017(c++11)编译结果为基准:
void f(int){} // 该函数的类型为: void(int)
void (*fa)(int) =f;
if (typeid(*fa) == typeid(void(int)))
cout << setw(20) << left << "fa的类型:" << typeid(fa).name() << endl
<< setw(20) << left << "void(int)的类型:" << typeid(void(int)).name() << endl;
cout << setw(20) << left << "f的类型" << typeid(f).name();
输出结果为:
fa的类型: | void (__cdecl*)(int) |
void(int)的类型: | void __cdecl(int) |
f的类型 | void __cdecl(int) |
可见: 函数名就代表了函数类型,同时也代表了函数的地址 ,它的值与对应函数指针的值相同,但是取地址的值不同:
cout << setw(10) << left << "f的值:" << f << endl
<< setw(10) << left << "fa的值:" << fa << endl
<< setw(10) << left << "&f的值:" << &f << endl
<< setw(10) << left << "&fa的值:" << &fa << endl;
输出结果为:
f的值: | 00007FF67F781442 |
fa的值: | 00007FF67F781442 |
&f的值: | 00007FF67F781442 |
&fa的值: | 000000064C10F9E8 |
零成本抽象
所谓“零成本抽象”有两个层面的意思:
不需要为没有使用到的语言特性付出代价。
使用某种语言特性,不会带来运行时的代价。
总的来说,这就是一种极度强调运行时性能,把所有解释抽象的工作都放在编译时完成的思路。
用对象内存布局举例,对于一个类,如果没有定义任何虚函数,也没有继承任何定义了虚函数的类,那么这个类的对象在内存中的布局与 C 语言的 struct 基本就是一致的,没有多余的虚表。
你没有用到虚函数带来的运行时多态特性,就不需要付出虚表带来的运行时开销。
你用到了“用类来抽象数据”这个特性,它没有带来任何额外的运行时开销,跟你分别单独操纵类的成员是一样的效率。
“零成本抽象”是一种语言特性的标准,C++ 有的特性是零成本的,但并不是所有的特性都是零成本的,比如刚才提到的虚表。
也不是说只要是“零成本抽象”就是好的语言特性,有些语言特性必须拿到运行时的信息(比如动态分派),有些语言特性做在运行时比编译时更好(比如真・泛型),还有的语言特性带来的运行时开销小到可以忽略(比如已经比较成熟了的异常机制),就不需要做成“零成本抽象”,把所有工夫都赶在编译时完成。
自己写码如何做到零代价抽象? C++设计者中告诉我们,C++ 中只有 2 个语言特性是不遵守零开销原则的,运行时类型识别(RTTI)和异常,所以实现零开销抽象的必要条件是不使用这两个特性。能否最终实现零开销,还是需要 100% 了解和掌控自己写的代码。
do while
do while 有两个用法:
第一种是 代码优化
中提到的用来简化多分支语句
第二种用法是和宏结合一起使用的,主要是为了让宏后面再加个分号,看起来更自然:
#define INSERT_CANCEL_POINT \
do { \
if(stop_) { \
qDebug() << "Cancel load images"; \
return; \
} \
} while(0)
include
include 用来包含一个指定的文件。include 一个文件和将其直接拷贝到源文件的效果相同。