一些比较好的网站:

还有一些待查看的知识:

变量的大小

变量环境大小

指针

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(unsigned long long)
匹配实数

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++ 程序中包含了对对象的创建、销毁、引用、访问以及操作。对象的创建有三种方式:

  1. 通过类型定义创建一个对象

  2. 通过 new 表达式创建对象

  3. 通过隐式创建一个对象(即当改变 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 作为函数参数引入时有三种方式:

  1. 使用模板参数

    template <typename T>
    void show(T obj){
       obj();
    }
    
    int main(){
       show([](){
          cout<<"hello, world";
       });
       return 0;
    }
  2. 使用退化的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 表达式有相同的调用方式
  3. 使用 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 用来在编译器对表达式/函数求值

大小端

两个十六进制数一共是八位[3]

大小端是指数据在内存中储存的形式,寄存器中总是大端的

大端就是书面上常用的形式,小端就是将字节顺序逆序排列。地址从左向右是升高的

大端小端没有谁优谁劣,各自优势便是对方劣势:

  • 小端模式 :强制转换数据不需要调整字节内容,1、2、4字节的存储方式一样。

  • 大端模式 :符号位的判定固定为第一个字节,容易判断正负。

一般计算机是小端序的,而通信是大端序的

移位运算

移位运算是在寄存器中的运算,与大小端无关。向左移位总是相当于乘以二的次幂,向右移位总是相当于除以二的次幂

内联函数

  • 内联函数必须放到头文件中,编译器必须能看到内敛函数的定义

  • inline 函数应该简洁,如果语句较多,不适合定义为内联函数

  • inline 函数中,一般不建议有循环、if 或 switch 语句,否则,函数定义时即使有 inline 关键字,编译器也可能会把该函数作为非内联函数处理。

  • inline 函数要在函数被调用之前声明。

当虚函数表现出多态时,inline 不起作用

内存对齐

使用 attributepackedattributealigned(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 一个文件和将其直接拷贝到源文件的效果相同。

Last moify: 2024-06-14 09:48:51
Build time:2025-07-18 09:41:42
Powered By asphinx