注意:这篇文章上次更新于1712天前,文章内容可能已经过时。
This article was last updated1712 days ago, the content may be outdated.
最近在看这个,由于原项目网页域名不太好记,就转载重新记录一下。
这里主要记录一些基础内容。
I’ve been reading this recently. Since the domain of the original project’s website is not easy to remember, I’m reposting and re-recording it here.
This mainly records some basic content.
Reprint Statement
Project address: https://github.com/Light-City/CPlusPlusThings
Original article link: https://light-city.club/sc/
const
Meaning of const
A constant type is a type declared with the type modifier const. The value of a variable or object of a constant type cannot be updated.
Role of const
- Can define constants
1 | const int a=100; |
-
类型检查
- const常量与
#define宏定义常量的区别:
~~const常量具有类型,编译器可以进行安全检查;#define宏定义没有数据类型,只是简单的字符串替换,不能进行安全检查。~~感谢两位大佬指出这里问题,见:issue
- const定义的变量只有类型为整数或枚举,且以常量表达式初始化时才能作为常量表达式。
- 其他情况下它只是一个
const限定的变量,不要将与常量混淆。
- const常量与
-
防止修改,起保护作用,增加程序健壮性
-
Type checking
- The difference between a const constant and a
#definemacro constant:
~~A const constant has a type, so the compiler can perform safety checks; a #define macro has no data type, it is just a simple string replacement and cannot be safety-checked.~~Thanks to two experts for pointing out the issue here, see: issue
- A variable defined with const can be used as a constant expression only when its type is integer or enumeration and it is initialized with a constant expression.
- In other cases it is just a
const-qualified variable; don’t confuse it with a constant.
- The difference between a const constant and a
-
Prevent modification, play a protective role, and increase program robustness
1 | void f(const int i){ |
-
可以节省空间,避免不必要的内存分配
- const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像
#define一样给出的是立即数。 - const定义的常量在程序运行过程中只有一份拷贝,而
#define定义的常量在内存中有若干个拷贝。
- const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是像
const对象默认为文件局部变量
注意:非const变量默认为extern。要使const变量能够在其他文件中访问,必须在文件中显式地指定它为extern。
未被const修饰的变量在不同文件的访问
-
Can save space and avoid unnecessary memory allocation
- From the assembly point of view, a const constant only gives the corresponding memory address, rather than an immediate value like
#definedoes. - A const constant has only one copy during program execution, while a
#defineconstant has several copies in memory.
- From the assembly point of view, a const constant only gives the corresponding memory address, rather than an immediate value like
const objects are file-local by default
Note: non-const variables are extern by default. To make a const variable accessible in other files, you must explicitly specify it as extern in the file.
Access to variables not modified by const across different files
1 | // file1.cpp |
const常量在不同文件的访问
Access to const constants across different files
1 | //extern_file1.cpp |
Summary:
You can see that variables not modified by const do not need an explicit extern declaration! However, const constants need an explicit extern declaration and also need initialization! Since a constant cannot be modified after it is defined, it must be initialized at definition time.
Defining Constants
1 | const int b = 10; |
There are two errors above:
- b is a constant, it cannot be changed!
- i is a constant, it must be initialized! (Since a constant cannot be modified after it is defined, it must be initialized at definition time.)
Pointers and const
There are four kinds of const related to pointers:
1 | const char * a; //指向const对象的指针或者说指向常量的指针。 |
小结:
如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量;
如果const位于*的右侧,const就是修饰指针本身,即指针本身是常量。
具体使用如下:
(1) 指向常量的指针
Summary:
If const is on the left of*, then const modifies the variable pointed to by the pointer, i.e., the pointee is constant;
if const is on the right of*, then const modifies the pointer itself, i.e., the pointer itself is constant.
The specific usages are as follows:
(1) Pointer to constant
1 | const int *ptr; |
ptr是一个指向int类型const对象的指针,const定义的是int类型,也就是ptr所指向的对象类型,而不是ptr本身,所以ptr可以不用赋初始值。但是不能通过ptr去修改所指对象的值。
除此之外,也不能使用void*指针保存const对象的地址,必须使用const void*类型的指针保存const对象的地址。
ptr is a pointer to a const object of type int. const qualifies the int type, i.e., the type of the object pointed to by ptr, not ptr itself, so ptr does not need to be initialized. But you cannot modify the value of the pointed-to object through ptr.
In addition, you cannot use a void* pointer to store the address of a const object; you must use a const void* pointer to store the address of a const object.
1 | const int p = 10; |
另外一个重点是:允许把非const对象的地址赋给指向const对象的指针。
将非const对象的地址赋给const对象的指针:
Another key point: the address of a non-const object can be assigned to a pointer to a const object.
Assigning the address of a non-const object to a pointer to a const object:
1 | const int *ptr; |
我们不能通过ptr指针来修改val的值,即使它指向的是非const对象!
我们不能使用指向const对象的指针修改基础对象,然而如果该指针指向了非const对象,可用其他方式修改其所指的对象。可以修改const指针所指向的值的,但是不能通过const对象指针来进行而已!如下修改:
We cannot modify the value of val through the ptr pointer, even though it points to a non-const object!
We cannot modify the underlying object using a pointer to a const object. However, if the pointer points to a non-const object, the object it points to can be modified in other ways. The value pointed to by a const pointer can be modified, but not through the const object pointer! Modify it as follows:
1 | int *ptr1 = &val; |
小结:
1.对于指向常量的指针,不能通过指针来修改对象的值。
2.不能使用void*指针保存const对象的地址,必须使用const void*类型的指针保存const对象的地址。
3.允许把非const对象的地址赋值给const对象的指针,如果要修改指针所指向的对象值,必须通过其他方式修改,不能直接通过当前指针直接修改。
(2) 常指针
const指针必须进行初始化,且const指针的值不能修改。
Summary:
1. For a pointer to a constant, the value of the object cannot be modified through the pointer.
2. You cannot use a void*pointer to store the address of a const object; you must use a const void*pointer.
3. The address of a non-const object can be assigned to a pointer to a const object. If you want to modify the pointed-to object’s value, you must modify it through other means, not directly through the current pointer.
(2) Constant pointer
A const pointer must be initialized, and the value of a const pointer cannot be modified.
1 |
|
上述修改ptr指针所指向的值,可以通过非const指针来修改。
最后,当把一个const常量的地址赋值给ptr时候,由于ptr指向的是一个变量,而不是const常量,所以会报错,出现:const int* -> int *错误!
The value pointed to by the ptr pointer above can be modified through a non-const pointer.
Finally, when assigning the address of a const constant to ptr, since ptr points to a variable rather than a const constant, an error occurs: const int* -> int * error!
1 |
|
上述若改为 const int *ptr或者改为const int *const ptr,都可以正常!
(3)指向常量的常指针
理解完前两种情况,下面这个情况就比较好理解了:
If we change it to const int *ptr or const int *const ptr as above, both work fine!
(3) Constant pointer to constant
After understanding the first two cases, the following case is easier to understand:
1 | const int p = 3; |
ptr是一个const指针,然后指向了一个int 类型的const对象。
函数中使用const
const修饰函数返回值
这个跟const修饰普通变量以及指针的含义基本相同:
(1)const int
ptr is a const pointer that points to a const object of type int.
Using const in functions
const modifies the return value of a function
This is basically the same as const modifying ordinary variables and pointers:
(1) const int
1 | const int func1(); |
这个本身无意义,因为参数返回本身就是赋值给其他的变量!
(2)const int*
This is meaningless by itself, because the returned value is assigned to another variable anyway!
(2) const int*
1 | const int* func2(); |
指针指向的内容不变。
(3)int *const
The content pointed to by the pointer cannot be changed.
(3) int *const
1 | int *const func2(); |
指针本身不可变。
const修饰函数参数
(1)传递过来的参数及指针本身在函数内不可变,无意义!
The pointer itself cannot be changed.
const modifies function parameters
(1) The passed parameter and the pointer itself are immutable inside the function; this is meaningless!
1 | void func(const int var); // 传递过来的参数不可变 |
表明参数在函数体内不能被修改,但此处没有任何意义,var本身就是形参,在函数内不会改变。包括传入的形参是指针也是一样。
输入参数采用“值传递”,由于函数将自动产生临时变量用于复制该参数,该输入参数本来就无需保护,所以不要加const 修饰。
(2)参数指针所指内容为常量不可变
It means the parameter cannot be modified inside the function body, but this is meaningless here: var itself is a formal parameter and will not change inside the function. The same applies when the passed formal parameter is a pointer.
Input parameters use “pass by value”. Since the function automatically creates a temporary variable to copy the parameter, the input parameter does not need protection, so do not add the const modifier.
(2) The content pointed to by the parameter pointer is a constant and cannot be changed
1 | void StringCopy(char *dst, const char *src); |
其中src 是输入参数,dst 是输出参数。给src加上const修饰后,如果函数体内的语句试图改动src的内容,编译器将指出错误。这就是加了const的作用之一。
(3)参数为引用,为了增加效率同时防止修改。
Here src is an input parameter and dst is an output parameter. After adding const to src, if any statement in the function body tries to change the content of src, the compiler will report an error. This is one of the effects of adding const.
(3) The parameter is a reference, to increase efficiency and prevent modification.
1 | void func(const A &a) |
对于非内部数据类型的参数而言,象void func(A a) 这样声明的函数注定效率比较低。因为函数体内将产生A 类型的临时对象用于复制参数a,而临时对象的构造、复制、析构过程都将消耗时间。
为了提高效率,可以将函数声明改为void func(A &a),因为“引用传递”仅借用一下参数的别名而已,不需要产生临
时对象。
但是函数void func(A &a) 存在一个缺点:
“引用传递”有可能改变参数a,这是我们不期望的。解决这个问题很容易,加const修饰即可,因此函数最终成为
void func(const A &a)。
以此类推,是否应将void func(int x) 改写为void func(const int &x),以便提高效率?完全没有必要,因为内部数
据类型的参数不存在构造、析构的过程,而复制也非常快,“值传递”和“引用传递”的效率几乎相当。
小结:
1.对于非内部数据类型的输入参数,应该将“值传递”的方式改为“const 引用传递”,目的是提高效率。例如将void func(A a) 改为void func(const A &a)。
2.对于内部数据类型的输入参数,不要将“值传递”的方式改为“const 引用传递”。否则既达不到提高效率的目的,又降低了函数的可理解性。例如void func(int x) 不应该改为void func(const int &x)。
以上解决了两个面试问题:
- 如果函数需要传入一个指针,是否需要为该指针加上const,把const加在指针不同的位置有什么区别;
- 如果写的函数需要传入的参数是一个复杂类型的实例,传入值参数或者引用参数有什么区别,什么时候需要为传入的引用参数加上const。
类中使用const
在一个类中,任何不会修改数据成员的函数都应该声明为const类型。如果在编写const成员函数时,不慎修改 数据成员,或者调用了其它非const成员函数,编译器将指出错误,这无疑会提高程序的健壮性。
使用const关键字进行说明的成员函数,称为常成员函数。只有常成员函数才有资格操作常量或常对象,没有使用const关键字进行说明的成员函数不能用来操作常对象。
对于类中的const成员变量必须通过初始化列表进行初始化,如下所示:
For parameters of non-built-in data types, a function declared like void func(A a) is bound to be inefficient. Because a temporary object of type A is created inside the function body to copy parameter a, and the construction, copying, and destruction of the temporary object all take time.
To improve efficiency, you can change the function declaration to void func(A &a), because “passing by reference” only borrows an alias of the parameter, without creating a temporary
object.
However, the function void func(A &a) has a drawback:
“passing by reference” may change parameter a, which is what we don’t want. This problem is easy to solve: just add const, so the function finally becomes
void func(const A &a).
By the same logic, should we rewrite void func(int x) as void func(const int &x) to improve efficiency? There is absolutely no need, because parameters of built-in data
types have no construction or destruction process, and copying is very fast, so “pass by value” and “pass by reference” are almost equally efficient.
Summary:
1. For input parameters of non-built-in data types, “pass by value” should be changed to “const reference pass” to improve efficiency. For example, change void func(A a) to void func(const A &a).
2. For input parameters of built-in data types, do not change “pass by value” to “const reference pass”. Otherwise, you won’t achieve the goal of improving efficiency, and you’ll reduce the readability of the function. For example, void func(int x) should not be changed to void func(const int &x).
The above solves two interview questions:
- If a function needs a pointer parameter, does the pointer need const, and what is the difference when const is placed at different positions of the pointer;
- If the parameter of a function is an instance of a complex type, what is the difference between passing by value and by reference, and when should const be added to the reference parameter.
Using const in a class
1 | class Apple{ |
const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数.
例如:
A const object can only access const member functions, while a non-const object can access any member function, including const member functions.
For example:
1 | //apple.cpp |
编译: g++ -o main main.cpp apple.cpp
结果:
Compile: g++ -o main main.cpp apple.cpp
Result:
1 | take func 1 |
上面getCount()方法中调用了一个add方法,而add方法并非const修饰,所以运行报错。也就是说const对象只能访问const成员函数。
而add方法又调用了const修饰的take方法,证明了非const对象可以访问任意的成员函数,包括const成员函数。
除此之外,我们也看到add的一个重载函数,也输出了两个结果,说明const对象默认调用const成员函数。
我们除了上述的初始化const常量用初始化列表方式外,也可以通过下面方法:
第一:将常量定义与static结合,也就是:
In the getCount() method above, an add method is called, but add is not const-qualified, so it fails at runtime. That is, a const object can only access const member functions.
The add method calls the const-qualified take method, proving that a non-const object can access any member function, including const member functions.
In addition, we also see that an overloaded version of add outputs two results, which shows that a const object calls const member functions by default.
Besides initializing const constants with an initializer list as above, we can also use the following methods:
First: combine the constant definition with static, that is:
1 | static const int apple_number |
第二:在外面初始化:
Second: initialize it outside:
1 | const int Apple::apple_number=10; |
当然,如果你使用c++11进行编译,直接可以在定义出初始化,可以直接写成:
Of course, if you compile with C++11, you can initialize it directly at the definition, which can be written as:
1 | static const int apple_number=10; |
这两种都在c++11中支持!
编译的时候加上-std=c++11即可!
这里提到了static,下面简单的说一下:
在C++中,static静态成员变量不能在类的内部初始化。在类的内部只是声明,定义必须在类定义体的外部,通常在类的实现文件中初始化。
在类中声明:
Both of these are supported in C++11!
Just add -std=c++11 when compiling!
Since static is mentioned here, let’s briefly talk about it:
In C++, static member variables cannot be initialized inside the class. Inside the class is only a declaration; the definition must be outside the class body, usually initialized in the class implementation file.
Declared in the class:
1 | static int ap; |
在类实现文件中使用:
Used in the class implementation file:
1 | int Apple::ap=666 |
对于此项,c++11不能进行声明并初始化,也就是上述使用方法。
static
当与不同类型一起使用时,Static关键字具有不同的含义。我们可以使用static关键字:
静态变量: 函数中的变量,类中的变量
静态类的成员: 类对象和类中的函数
现在让我们详细看一下静态的这些用法:
静态变量
- 函数中的静态变量
当变量声明为static时,空间将在程序的生命周期内分配。即使多次调用该函数,静态变量的空间也只分配一次,前一次调用中的变量值通过下一次函数调用传递。这对于在C / C ++或需要存储先前函数状态的任何其他应用程序非常有用。
For this, C++11 cannot declare and initialize at the same time, i.e., the usage method above.
static
When used with different types, the static keyword has different meanings. We can use the static keyword:
Static variables: variables in functions, variables in classes
Static class members: class objects and functions in classes
Now let’s take a detailed look at these usages of static:
Static variables
- Static variables in functions
When a variable is declared as static, its space will be allocated for the lifetime of the program. Even if the function is called multiple times, the space of the static variable is only allocated once, and the variable value from the previous call is passed to the next function call. This is very useful in C/C++ or any other application that needs to store the state of previous function calls.
1 |
|
输出:
Output:
1 | 0 1 2 3 4 |
您可以在上面的程序中看到变量count被声明为static。因此,它的值通过函数调用来传递。每次调用函数时,都不会对变量计数进行初始化。
- 类中的静态变量
由于声明为static的变量只被初始化一次,因为它们在单独的静态存储中分配了空间,因此类中的静态变量**由对象共享。**对于不同的对象,不能有相同静态变量的多个副本。也是因为这个原因,静态变量不能使用构造函数初始化。
You can see in the program above that the variable count is declared as static. Therefore, its value is passed through function calls. Each time the function is called, the variable count is not re-initialized.
- Static variables in classes
Since variables declared as static are initialized only once, because they are allocated space in a separate static storage, static variables in a class are shared by objects. For different objects, there cannot be multiple copies of the same static variable. For this reason, static variables cannot be initialized using constructors.
1 |
|
您可以在上面的程序中看到我们已经尝试为多个对象创建静态变量i的多个副本。但这并没有发生。因此,类中的静态变量应由用户使用类外的类名和范围解析运算符显式初始化,如下所示:
You can see in the program above that we tried to create multiple copies of the static variable i for multiple objects. But this did not happen. Therefore, static variables in a class should be explicitly initialized by the user using the class name and the scope resolution operator outside the class, as follows:
1 |
|
输出:
Output:
1 | 1 |
静态成员
- 类对象为静态
就像变量一样,对象也在声明为static时具有范围,直到程序的生命周期。
考虑以下程序,其中对象是非静态的。
Static members
- Class objects are static
Like variables, objects also have scope until the program’s lifetime when declared as static.
Consider the following program, in which the object is non-static.
1 |
|
输出:
Output:
1 | Inside Constructor |
在上面的程序中,对象在if块内声明为非静态。因此,变量的范围仅在if块内。因此,当创建对象时,将调用构造函数,并且在if块的控制权越过析构函数的同时调用,因为对象的范围仅在声明它的if块内。
如果我们将对象声明为静态,现在让我们看看输出的变化。
In the program above, the object is declared as non-static inside the if block. Therefore, the variable’s scope is only within the if block. So when the object is created, the constructor is called, and when control passes beyond the if block, the destructor is called at the same time, because the object’s scope is only within the if block where it was declared.
If we declare the object as static, let’s now see how the output changes.
1 |
|
输出:
Output:
1 | Inside Constructor |
您可以清楚地看到输出的变化。现在,在main结束后调用析构函数。这是因为静态对象的范围是贯穿程序的生命周期。
- 类中的静态函数
就像类中的静态数据成员或静态变量一样,静态成员函数也不依赖于类的对象。我们被允许使用对象和’.'来调用静态成员函数。但建议使用类名和范围解析运算符调用静态成员。
允许静态成员函数仅访问静态数据成员或其他静态成员函数,它们无法访问类的非静态数据成员或成员函数。
You can clearly see the change in the output. Now, the destructor is called after main ends. This is because the scope of a static object spans the entire lifetime of the program.
- Static functions in classes
Like static data members or static variables in a class, static member functions do not depend on class objects. We are allowed to call static member functions using objects and ‘.’. But it is recommended to call static members using the class name and the scope resolution operator.
Static member functions are allowed to access only static data members or other static member functions; they cannot access the class’s non-static data members or member functions.
1 |
|
输出:
Output:
1 | Welcome to Apple! |
this
this指针
相信在坐的很多人,都在学Python,对于Python来说有self,类比到C++中就是this指针,那么下面一起来深入分析this指针在类中的使用!
首先来谈谈this指针的用处:
(1)一个对象的this指针并不是对象本身的一部分,不会影响sizeof(对象)的结果。
(2)this作用域是在类内部,当在类的非静态成员函数中访问类的非静态成员的时候,编译器会自动将对象本身的地址作为一个隐含参数传递给函数。也就是说,即使你没有写上this指针,编译器在编译的时候也是加上this的,它作为非静态成员函数的隐含形参,对各成员的访问均通过this进行。
其次,this指针的使用:
(1)在类的非静态成员函数中返回类对象本身的时候,直接使用 return *this。
(2)当参数与成员变量名相同时,如this->n = n (不能写成n = n)。
另外,在网上大家会看到this会被编译器解析成A *const,A const *,究竟是哪一个呢?下面通过断点调试分析:
现有如下例子:
this
The this pointer
I believe many of you are learning Python. Python has self, and the analog in C++ is the this pointer. Let’s analyze in depth the use of the this pointer in classes!
First, let’s talk about the purpose of the this pointer:
(1) An object’s this pointer is not part of the object itself, and does not affect the result of sizeof(object).
(2) The scope of this is inside the class. When accessing non-static members of the class in a non-static member function, the compiler automatically passes the address of the object itself as an implicit parameter to the function. In other words, even if you don’t write the this pointer, the compiler adds this during compilation. It acts as the implicit formal parameter of non-static member functions, and all member accesses are performed through this.
Second, the use of the this pointer:
(1) When returning the class object itself in a non-static member function, directly use return *this.
(2) When a parameter has the same name as a member variable, such as this->n = n (you cannot write n = n).
In addition, online you’ll see that this is parsed by the compiler as A *const or A const *; which one is it exactly? Let’s analyze it through breakpoint debugging:
There is an example as follows:
1 |
|
对于这个简单的程序,相信大家没得问题吧,就是定义了一个类,然后初始化构造函数,并获取这个人的年龄,设置后,再获取!
为了验证this指针是哪一个,现在在add_age处添加断点,运行后如下:


会发现编译器自动为我们加上A* const,而不是A const *this!
紧接着,上述还有个常函数,那么我们在对get_age添加断点,如下:

会发现编译器把上述的this,变为const A* const,这个大家也能想到,因为这个函数是const函数,那么针对const函数,它只能访问const变量与const函数,不能修改其他变量的值,所以需要一个this指向不能修改的变量,那就是const A*,又由于本身this是const指针,所以就为const A* const!
总结:this在成员函数的开始执行前构造,在成员的执行结束后清除。上述的get_age函数会被解析成get_age(const A * const this),add_age函数会被解析成add_age(A* const this,int a)。在C++中类和结构是只有一个区别的:类的成员默认是private,而结构是public。this是类的指针,如果换成结构,那this就是结构的指针了。
inline
类中内联
头文件中声明方法
I believe everyone understands this simple program: it defines a class, initializes the constructor, gets this person’s age, sets it, and then gets it again!
To verify which type the this pointer is, add a breakpoint at add_age now. After running, it looks like this:


You’ll find that the compiler automatically adds A* const for us, rather than A const *this!
Next, the code above also has a const function. Let’s add a breakpoint at get_age, as follows:

You’ll find that the compiler turns this into const A* const. As you might expect, since this function is a const function, a const function can only access const variables and const functions and cannot modify the values of other variables, so it needs a this that points to a variable that cannot be modified, namely const A*. And since this itself is a const pointer, it becomes const A* const!
Summary: this is constructed before the member function starts executing and cleared after the member finishes executing. The get_age function above is parsed as get_age(const A * const this), and the add_age function is parsed as add_age(A* const this, int a). In C++, there is only one difference between a class and a struct: class members are private by default, while struct members are public. this is a class pointer; if it were a struct, this would be a struct pointer.
inline
Inline in classes
Declare methods in the header file
1 |
|
实现文件中定义内联函数:
Define inline functions in the implementation file:
1 |
|
内联能提高函数效率,但并不是所有的函数都定义成内联函数!内联是以代码膨胀(复制)为代价,仅仅省去了函数调用的开销,从而提高函数的执行效率。
-
如果执行函数体内代码的时间相比于函数调用的开销较大,那么效率的收货会更少!
-
另一方面,每一处内联函数的调用都要复制代码,将使程序的总代码量增大,消耗更多的内存空间。
以下情况不宜用内联:
(1)如果函数体内的代码比较长,使得内联将导致内存消耗代价比较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
虚函数(virtual)可以是内联函数(inline)吗?
- 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联。
- 内联是在编译期建议编译器内联,而虚函数的多态性在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时(运行期)不可以内联。
inline virtual唯一可以内联的时候是:编译器知道所调用的对象是哪个类(如Base::who()),这只有在编译器具有实际对象而不是对象的指针或引用时才会发生。
Inline can improve function efficiency, but not all functions should be defined as inline! Inline trades code bloat (copying) for the elimination of function call overhead, thereby improving the execution efficiency of the function.
-
If the time to execute the function body is large compared to the function call overhead, then the efficiency gain will be less!
-
On the other hand, every inline function call copies the code, increasing the total code size of the program and consuming more memory.
Inline should not be used in the following cases:
(1) If the code in the function body is relatively long, inlining will result in a relatively high memory cost.
(2) If there is a loop in the function body, then the time to execute the function body code is greater than the function call overhead.
Can virtual functions be inline functions?
- A virtual function can be an inline function; inline can modify virtual functions, but when a virtual function exhibits polymorphism, it cannot be inlined.
- Inline is a compile-time suggestion to the compiler to inline, while the polymorphism of virtual functions happens at runtime. The compiler cannot know at runtime which code will be called, so a virtual function cannot be inlined when it exhibits polymorphism (at runtime).
- The only time
inline virtualcan be inlined is when the compiler knows which class the called object belongs to (such asBase::who()), which only happens when the compiler has the actual object rather than a pointer or reference to the object.
1 |
|
sizeof
类大小计算
首先来个总结,然后下面给出实际例子,实战!
- 空类的大小为1字节
- 一个类中,虚函数本身、成员函数(包括静态与非静态)和静态数据成员都是不占用类对象的存储空间。
- 对于包含虚函数的类,不管有多少个虚函数,只有一个虚指针,vptr的大小。
- 普通继承,派生类继承了所有基类的函数与成员,要按照字节对齐来计算大小
- 虚函数继承,不管是单继承还是多继承,都是继承了基类的vptr。(32位操作系统4字节,64位操作系统 8字节)!
- 虚继承,继承基类的vptr。
原则1
sizeof
Class size calculation
First a summary, then actual examples below, hands-on!
- The size of an empty class is 1 byte
- In a class, virtual functions themselves, member functions (both static and non-static), and static data members do not occupy the storage space of class objects.
- For a class containing virtual functions, no matter how many virtual functions there are, there is only one virtual pointer, the size of vptr.
- In ordinary inheritance, the derived class inherits all base class functions and members, and the size should be calculated according to byte alignment
- In virtual function inheritance, whether single inheritance or multiple inheritance, the base class’s vptr is inherited. (4 bytes on 32-bit OS, 8 bytes on 64-bit OS)!
- In virtual inheritance, the base class’s vptr is inherited.
Principle 1
1 | /** |
1 | /** |
1 | /** |
1 | /** |
1 | /** |
Pure virtual functions and abstract classes
Pure virtual functions and abstract classes
A pure virtual function (or abstract function) in C++ is a virtual function we haven’t implemented! We only need to declare it! A pure virtual function is declared by assigning 0 in the declaration!
1 | // 抽象类 |
- 纯虚函数:没有函数体的虚函数
- 抽象类:包含纯虚函数的类
抽象类只能作为基类来派生新类使用,不能创建抽象类的对象,抽象类的指针和引用->由抽象类派生出来的类的对象!
- Pure virtual function: a virtual function without a function body
- Abstract class: a class containing pure virtual functions
An abstract class can only be used as a base class to derive new classes; you cannot create objects of an abstract class. Pointers and references of an abstract class point to objects of classes derived from the abstract class!
1 | /** |
1 |
|
Implementing abstract classes
In an abstract class: pure virtual functions can be called inside member functions, but cannot be used inside constructors/destructors.
If a class is derived from an abstract class, it must implement all pure virtual functions in the base class to become a non-abstract class.
1 | // A为抽象类 |
1 | // 抽象类至少包含一个纯虚函数 |
- 抽象类类型的指针和引用
- Pointers and references of abstract class type
1 |
|
- 如果我们不在派生类中覆盖纯虚函数,那么派生类也会变成抽象类
- If we don’t override the pure virtual function in the derived class, the derived class also becomes an abstract class
1 |
|
- 抽象类可以有构造函数
- An abstract class can have constructors
1 |
|
- 构造函数不能是虚函数,而析构函数可以是虚析构函数
- Constructors cannot be virtual functions, while destructors can be virtual destructors
1 |
|
When a base class pointer points to a derived class object and the object is deleted, we may want the appropriate destructor to be called.
If the destructor is not virtual, only the base class destructor can be called.
Complete example
The abstract class is implemented by inheritance in a derived class!
1 |
|
vptr_vtable
基础理论
为了实现虚函数,C ++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表有时会使用其他名称,例如“vtable”,“虚函数表”,“虚方法表”或“调度表”。
虚拟表实际上非常简单,虽然用文字描述有点复杂。首先,每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数。
其次,编译器还会添加一个隐藏指向基类的指针,我们称之为vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。
因此,它使每个类对象的分配大一个指针的大小。这也意味着vptr由派生类继承,这很重要。
实现与内部结构
下面我们来看自动与手动操纵vptr来获取地址与调用虚函数!
开始看代码之前,为了方便大家理解,这里给出调用图:

代码全部遵循标准的注释风格,相信大家看了就会明白,不明白的话,可以留言!
vptr_vtable
Basic theory
To implement virtual functions, C++ uses a special form of late binding called a virtual table. The virtual table is a lookup table used to resolve function calls in a dynamic/late binding manner. The virtual table sometimes goes by other names, such as “vtable”, “virtual function table”, “virtual method table”, or “dispatch table”.
The virtual table is actually very simple, although it’s a bit complicated to describe in words. First, every class that uses virtual functions (or is derived from a class that uses virtual functions) has its own virtual table. The table is just a static array set up by the compiler at compile time. The virtual table contains one entry for each virtual function that can be called by an object of the class. Each entry in this table is just a function pointer pointing to the derived function accessible to that class.
Second, the compiler also adds a hidden pointer to the base class, which we call vptr. The vptr is automatically set when a class instance is created, so that it points to the class’s virtual table. Unlike the this pointer, which is actually a function parameter used by the compiler to resolve self-references, the vptr is a real pointer.
Therefore, it makes each class object allocation larger by one pointer size. It also means the vptr is inherited by derived classes, which is important.
Implementation and internal structure
Below we’ll look at automatically and manually manipulating vptr to get addresses and call virtual functions!
Before looking at the code, to make it easier for everyone to understand, here is the call diagram:

The code follows the standard comment style throughout. I believe you’ll understand it at a glance; if not, feel free to leave a comment!
1 | /** |
运行结果:
Run result:
1 | 基类对象直接调用 |
我们发现C++的动态多态性是通过虚函数来实现的。简单的说,通过virtual函数,指向子类的基类指针可以调用子类的函数。例如,上述通过基类指针指向派生类实例,并调用虚函数,将上述代码简化为:
We can see that C++'s dynamic polymorphism is implemented through virtual functions. Simply put, through virtual functions, a base class pointer pointing to a derived class can call the derived class’s functions. For example, the above code that points a base class pointer to a derived class instance and calls virtual functions can be simplified to:
1 | Base *pt = new Derived(); // 基类指针指向派生类实例 |
其过程为:首先程序识别出fun1()是个虚函数,其次程序使用pt->vptr来获取Derived的虚拟表。第三,它查找Derived虚拟表中调用哪个版本的fun1()。这里就可以发现调用的是Derived::fun1()。因此pt->fun1()被解析为Derived::fun1()!
除此之外,上述代码大家会看到,也包含了手动获取vptr地址,并调用vtable中的函数,那么我们一起来验证一下上述的地址与真正在自动调用vtable中的虚函数,比如上述pt->fun1()的时候,是否一致!
这里采用gdb调试,在编译的时候记得加上-g。
通过gdb vptr进入gdb调试页面,然后输入b Derived::fun1对fun1打断点,然后通过输入r运行程序到断点处,此时我们需要查看调用栈中的内存地址,通过disassemable fun1可以查看当前有关fun1中的相关汇编代码,我们看到了0x0000000000400ea8,然后再对比上述的结果会发现与手动调用的fun1一致,fun2类似,以此证明代码正确!
gdb调试信息如下:
The process is: first the program recognizes that fun1() is a virtual function; second, the program uses pt->vptr to get Derived’s virtual table. Third, it looks up which version of fun1() to call in Derived’s virtual table. Here we can find that Derived::fun1() is called. Therefore, pt->fun1() is resolved to Derived::fun1()!
In addition, you may have noticed that the code above also manually gets the vptr address and calls functions in the vtable. Let’s verify whether the addresses above are consistent with the virtual functions actually called automatically through the vtable, such as pt->fun1() above!
Here we use gdb for debugging. Remember to add -g when compiling.
Enter the gdb debugging page via gdb vptr, then enter b Derived::fun1 to set a breakpoint on fun1, then enter r to run the program to the breakpoint. At this point we need to look at the memory address in the call stack. Via disassemble fun1 we can view the relevant assembly code of fun1, and we see 0x0000000000400ea8. Comparing it with the result above, we find it is consistent with the manually called fun1; fun2 is similar, which proves the code is correct!
The gdb debugging information is as follows:
1 | (gdb) b Derived::fun1 |
volatile
被 volatile 修饰的变量,在对其进行读写操作时,会引发一些可观测的副作用。而这些可观测的副作用,是由程序之外的因素决定的。
volatile应用
(1)并行设备的硬件寄存器(如状态寄存器)。
假设要对一个设备进行初始化,此设备的某一个寄存器为0xff800000。
volatile
Variables modified by volatile, when read or written, will cause some observable side effects. And these observable side effects are determined by factors outside the program.
Applications of volatile
(1) Hardware registers of parallel devices (such as status registers).
Suppose we want to initialize a device, and one of its registers is 0xff800000.
1 | int *output = (unsigned int *)0xff800000; //定义一个IO端口; |
经过编译器优化后,编译器认为前面循环半天都是废话,对最后的结果毫无影响,因为最终只是将output这个指针赋值为 9,所以编译器最后给你编译编译的代码结果相当于:
After compiler optimization, the compiler considers the previous loop to be a waste of time with no effect on the final result, because in the end it only assigns 9 to the output pointer. So the compiled code is equivalent to:
1 | int init(void) |
如果你对此外部设备进行初始化的过程是必须是像上面代码一样顺序的对其赋值,显然优化过程并不能达到目的。反之如果你不是对此端口反复写操作,而是反复读操作,其结果是一样的,编译器在优化后,也许你的代码对此地址的读操作只做了一次。然而从代码角度看是没有任何问题的。这时候就该使用volatile通知编译器这个变量是一个不稳定的,在遇到此变量时候不要优化。
(2)一个中断服务子程序中访问到的变量;
If the initialization process of this external device must assign values sequentially as in the code above, the optimization obviously fails to achieve the goal. Conversely, if instead of repeatedly writing to this port you repeatedly read from it, the result is the same: after optimization, the compiler may perform the read from this address only once. However, from the code’s perspective, there is nothing wrong. This is when you should use volatile to tell the compiler that this variable is unstable and should not be optimized when encountered.
(2) A variable accessed in an interrupt service routine;
1 | static int i=0; |
上面示例程序的本意是产生中断时,由中断服务子程序IRS响应中断,变更程序变量i,使在main函数中调用dosomething函数,但是,由于编译器判断在main函数里面没有修改过i,因此可能只执行一次对从i到某寄存器的读操作,然后每次if判断都只使用这个寄存器里面的“i副本”,导致dosomething永远不会被调用。如果将变量i加上volatile修饰,则编译器保证对变量i的读写操作都不会被优化,从而保证了变量i被外部程序更改后能及时在原程序中得到感知。
(3)多线程应用中被多个任务共享的变量。
当多个线程共享某一个变量时,该变量的值会被某一个线程更改,应该用 volatile 声明。作用是防止编译器优化把变量从内存装入CPU寄存器中,当一个线程更改变量后,未及时同步到其它线程中导致程序出错。volatile的意思是让编译器每次操作该变量时一定要从内存中真正取出,而不是使用已经存在寄存器中的值。示例如下:
The intent of the sample program above is that when an interrupt occurs, the interrupt service routine IRS responds to the interrupt and changes the program variable i, so that the dosomething function is called in main. However, since the compiler determines that i is never modified in main, it may perform the read from i to a register only once, and then every if judgment only uses this “i copy” in the register, causing dosomething to never be called. If the variable i is modified with volatile, the compiler guarantees that read and write operations on i will not be optimized, ensuring that after i is changed by an external program, the change can be noticed in the original program in time.
(3) Variables shared by multiple tasks in multi-threaded applications.
When multiple threads share a variable, the value of that variable can be changed by one thread, and it should be declared with volatile. Its purpose is to prevent the compiler from optimizing the variable from memory into a CPU register, which could cause program errors when one thread changes the variable and the change is not synchronized to other threads in time. volatile means that every time the compiler operates on this variable, it must really read it from memory, rather than using the value already in the register. An example is as follows:
1 | volatile bool bStop=false; //bStop 为共享全局变量 |
要想通过第二个线程终止第一个线程循环,如果bStop不使用volatile定义,那么这个循环将是一个死循环,因为bStop已经读取到了寄存器中,寄存器中bStop的值永远不会变成FALSE,加上volatile,程序在执行时,每次均从内存中读出bStop的值,就不会死循环了。
是否了解volatile的应用场景是区分C/C++程序员和嵌入式开发程序员的有效办法,搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,这些都要求用到volatile变量,不懂得volatile将会带来程序设计的灾难。
volatile常见问题
下面的问题可以看一下面试者是不是直正了解volatile。
(1)一个参数既可以是const还可以是volatile吗?为什么?
可以。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
(2)一个指针可以是volatile吗?为什么?
可以。尽管这并不常见。一个例子是当一个中断服务子程序修该一个指向一个buffer的指针时。
(3)下面的函数有什么错误?
If we want the second thread to terminate the loop of the first thread, and bStop is not defined with volatile, then the loop will be an infinite loop, because bStop has already been read into the register, and the value of bStop in the register will never become FALSE. With volatile, the program reads the value of bStop from memory every time during execution, so there will be no infinite loop.
Whether you understand the application scenarios of volatile is an effective way to distinguish C/C++ programmers from embedded development programmers. Embedded folks often deal with hardware, interrupts, RTOS, and so on, all of which require volatile variables. Not understanding volatile will bring disaster to program design.
Common questions about volatile
The following questions test whether the interviewee really understands volatile.
(1) Can a parameter be both const and volatile? Why?
Yes. One example is a read-only status register. It is volatile because it may be changed unexpectedly. It is const because the program should not try to modify it.
(2) Can a pointer be volatile? Why?
Yes. Although it’s not common. One example is when an interrupt service routine modifies a pointer pointing to a buffer.
(3) What’s wrong with the following function?
1 | int square(volatile int *ptr) |
这段代码有点变态,其目的是用来返回指针ptr指向值的平方,但是,由于ptr指向一个volatile型参数,编译器将产生类似下面的代码:
This code is a bit tricky. Its purpose is to return the square of the value pointed to by ptr. However, since ptr points to a volatile parameter, the compiler will generate code similar to the following:
1 | int square(volatile int *ptr) |
由于*ptr的值可能被意想不到地改变,因此a和b可能是不同的。结果,这段代码可能返回的不是你所期望的平方值!正确的代码如下:
Since the value of *ptr may be changed unexpectedly, a and b may be different. As a result, this code may not return the square value you expect! The correct code is as follows:
1 | long square(volatile int *ptr) |
volatile使用
-
volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素(操作系统、硬件、其它线程等)更改。所以使用 volatile 告诉编译器不应对这样的对象进行优化。
-
volatile 关键字声明的变量,每次访问时都必须从内存中取出值(没有被 volatile 修饰的变量,可能由于编译器的优化,从 CPU 寄存器中取值)
-
const 可以是 volatile (如只读的状态寄存器)
-
指针可以是 volatile
代码学习:
Using volatile
-
The volatile keyword is a type modifier. A variable declared with it indicates that it can be changed by some factors unknown to the compiler (operating system, hardware, other threads, etc.). So using volatile tells the compiler not to optimize such objects.
-
A variable declared with the volatile keyword must fetch its value from memory on every access (variables not modified with volatile may take their value from a CPU register due to compiler optimization)
-
const can be volatile (such as a read-only status register)
-
Pointers can be volatile
Code study:
1 | /* Compile code without optimization option */ |
1 | /* Compile code with optimization option */ |
assert
第一个断言案例
断言,是宏,而非函数。
assert 宏的原型定义在 <assert.h>(C)、
可以通过定义 NDEBUG 来关闭 assert,但是需要在源代码的开头,include <assert.h> 之前。
assert
The first assertion example
An assertion is a macro, not a function.
The prototype of the assert macro is defined in <assert.h> © and
assert can be disabled by defining NDEBUG, but this needs to be done at the beginning of the source code, before including <assert.h>.
1 | void assert(int expression); |
1 |
|
输出:
Output:
1 | assert: assert.c:13: main: Assertion 'x==7' failed. |
可以看到输出会把源码文件,行号错误位置,提示出来!
断言与正常错误处理
- 断言主要用于检查逻辑上不可能的情况。
例如,它们可用于检查代码在开始运行之前所期望的状态,或者在运行完成后检查状态。与正常的错误处理不同,断言通常在运行时被禁用。
- 忽略断言,在代码开头加上:
You can see that the output will indicate the source file, the line number and the error location!
Assertions vs. normal error handling
- Assertions are mainly used to check logically impossible situations.
For example, they can be used to check the state expected before the code starts running, or to check the state after the run completes. Unlike normal error handling, assertions are usually disabled at runtime.
- To ignore assertions, add this at the beginning of the code:
1 |
1 |
|
位域
Bit field 是什么?
“ 位域 “ 或 “ 位段 “(Bit field)为一种数据结构,可以把数据以位的形式紧凑的储存,并允许程序员对此结构的位进行操作。这种数据结构的一个好处是它可以使数据单元节省储存空间,当程序需要成千上万个数据单元时,这种方法就显得尤为重要。第二个好处是位段可以很方便的访问一个整数值的部分内容从而可以简化程序源代码。而这种数据结构的缺点在于,位段实现依赖于具体的机器和系统,在不同的平台可能有不同的结果,这导致了位段在本质上是不可移植的。
- 位域在内存中的布局是与机器有关的
- 位域的类型必须是整型或枚举类型,带符号类型中的位域的行为将因具体实现而定
- 取地址运算符(&)不能作用于位域,任何指针都无法指向类的位域
位域使用
位域通常使用结构体声明, 该结构声明为每个位域成员设置名称,并决定其宽度:
Bit fields
What is a bit field?
A “bit field” (or “bit segment”) is a data structure that can store data compactly in bit form and allows programmers to operate on the bits of this structure. One benefit of this data structure is that it allows data units to save storage space; when a program needs tens of thousands of data units, this approach becomes especially important. The second benefit is that bit segments can conveniently access part of an integer value, thus simplifying the program source code. The drawback of this data structure is that bit segment implementation depends on the specific machine and system, and results may differ on different platforms, which makes bit segments essentially non-portable.
- The layout of bit fields in memory is machine-dependent
- The type of a bit field must be an integer or enumeration type; the behavior of bit fields in signed types is implementation-defined
- The address-of operator (&) cannot be applied to bit fields, and no pointer can point to a class’s bit field
Using bit fields
Bit fields are usually declared with a struct, which sets a name for each bit field member and determines its width:
1 | struct bit_field_name |
| Elements | Description |
|---|---|
| bit_field_name | 位域结构名 |
| type | 位域成员的类型,必须为 int、signed int 或者 unsigned int 类型 |
| member_name | 位域成员名 |
| width | 规定成员所占的位数 |
例如声明如下一个位域:
| Elements | Description |
|---|---|
| bit_field_name | Name of the bit field structure |
| type | Type of the bit field member, must be int, signed int or unsigned int |
| member_name | Name of the bit field member |
| width | The number of bits occupied by the member |
For example, declare a bit field as follows:
1 | struct _PRCODE |
该定义使 prcode包含 2 个 2 Bits 位域和 1 个 8 Bits 位域,我们可以使用结构体的成员运算符对其进行赋值
This definition makes prcode contain two 2-bit bit fields and one 8-bit bit field. We can assign values to them using the struct’s member operator
1 | prcode.code1 = 0; |
赋值时要注意值的大小不能超过位域成员的容量,例如 prcode.code3 为 8 Bits 的位域成员,其容量为 2^8 = 256,即赋值范围应为 [0,255]。
位域的大小和对齐
位域的大小
例如以下位域:
When assigning, note that the value must not exceed the capacity of the bit field member. For example, prcode.code3 is an 8-bit bit field member with a capacity of 2^8 = 256, so the assignment range should be [0, 255].
Size and alignment of bit fields
Size of bit fields
For example, the following bit field:
1 | struct box |
该位域结构体中间有一个未命名的位域,占据 3 Bits,仅起填充作用,并无实际意义。 填充使得该结构总共使用了 8 Bits。但 C 语言使用 unsigned int 作为位域的基本单位,即使一个结构的唯一成员为 1 Bit 的位域,该结构大小也和一个 unsigned int 大小相同。 有些系统中,unsigned int 为 16 Bits,在 x86 系统中为 32 Bits。文章以下均默认 unsigned int 为 32 Bits。
位域的对齐
一个位域成员不允许跨越两个 unsigned int 的边界,如果成员声明的总位数超过了一个 unsigned int 的大小, 那么编辑器会自动移位位域成员,使其按照 unsigned int 的边界对齐。
例如:
In the middle of this bit field struct there is an unnamed bit field occupying 3 bits, which only serves as padding and has no practical meaning. The padding makes the struct use 8 bits in total. But C uses unsigned int as the basic unit of bit fields; even if the only member of a struct is a 1-bit bit field, the struct size is the same as an unsigned int. In some systems, unsigned int is 16 bits; in x86 systems it is 32 bits. The rest of this article assumes unsigned int is 32 bits.
Alignment of bit fields
A bit field member is not allowed to cross the boundary of two unsigned ints. If the total number of bits declared by the members exceeds the size of one unsigned int, the compiler will automatically shift the bit field member to align it on the unsigned int boundary.
For example:
1 | struct stuff |
field1 + field2 = 34 Bits,超出 32 Bits, 编译器会将field2移位至下一个 unsigned int 单元存放, stuff.field1 和 stuff.field2 之间会留下一个 2 Bits 的空隙, stuff.field3 紧跟在 stuff.field2 之后,该结构现在大小为 2 * 32 = 64 Bits。
这个空洞可以用之前提到的未命名的位域成员填充,我们也可以使用一个宽度为 0 的未命名位域成员令下一位域成员与下一个整数对齐。
例如:
field1 + field2 = 34 bits, exceeding 32 bits, so the compiler shifts field2 to the next unsigned int unit. A 2-bit gap is left between stuff.field1 and stuff.field2, and stuff.field3 immediately follows stuff.field2. The struct now has a size of 2 * 32 = 64 bits.
This hole can be filled with the unnamed bit field member mentioned earlier. We can also use an unnamed bit field member with width 0 to align the next bit field member with the next integer.
For example:
1 | struct stuff |
这里 stuff.field1 与 stuff.field2 之间有一个 2 Bits 的空隙,stuff.field3 则存储在下一个 unsigned int 中,该结构现在大小为 3 * 32 = 96 Bits。
学习代码见:
Here there is a 2-bit gap between stuff.field1 and stuff.field2, and stuff.field3 is stored in the next unsigned int. The struct now has a size of 3 * 32 = 96 bits.
See the study code:
1 |
|
Initialization of bit fields and bit remapping
Initialization
Initializing bit fields is the same as initializing ordinary structs. Two methods are listed here, as follows:
1 | struct stuff s1= {20,8,6}; |
或者直接为位域成员赋值
Or directly assign values to the bit field members
1 | struct stuff s1; |
1 | struct box { |
1 | int* p = (int *) &b1; // 将 "位域结构体的地址" 映射至 "整形(int*) 的地址" |
利用联合 (union) 将 32 Bits 位域 重映射至 unsigned int 型
先简单介绍一下联合
“联合” 是一种特殊的类,也是一种构造类型的数据结构。在一个 “联合” 内可以定义多种不同的数据类型, 一个被说明为该 “联合” 类型的变量中,允许装入该 “联合” 所定义的任何一种数据,这些数据共享同一段内存,以达到节省空间的目的
“联合” 与 “结构” 有一些相似之处。但两者有本质上的不同。在结构中各成员有各自的内存空间, 一个结构变量的总长度是各成员长度之和(空结构除外,同时不考虑边界调整)。而在 “联合” 中,各成员共享一段内存空间, 一个联合变量的长度等于各成员中最长的长度。应该说明的是, 这里所谓的共享不是指把多个成员同时装入一个联合变量内, 而是指该联合变量可被赋予任一成员值,但每次只能赋一种值, 赋入新值则冲去旧值。
我们可以声明以下联合:
Using a union to remap a 32-bit bit field to unsigned int
First, a brief introduction to unions
A “union” is a special class and also a constructed data structure. Multiple different data types can be defined inside a “union”. A variable declared as this “union” type is allowed to hold any one of the data types defined by the “union”; these data share the same memory segment, in order to save space.
A “union” and a “struct” have some similarities, but they are fundamentally different. In a struct, each member has its own memory space, and the total length of a struct variable is the sum of the lengths of its members (empty structs excluded, and boundary adjustment not considered). In a “union”, members share a memory segment, and the length of a union variable equals the length of its longest member. It should be noted that “sharing” here does not mean loading multiple members into a union variable at the same time; rather, the union variable can be assigned any one of the member values, but only one value at a time — assigning a new value erases the old one.
We can declare the following union:
1 | union u_box { |
x86 系统中 unsigned int 和 box 都为 32 Bits, 通过该联合使 st_box 和 ui_box 共享一块内存。具体位域中哪一位与 unsigned int 哪一位相对应,取决于编译器和硬件。
利用联合将位域归零,代码如下:
In x86 systems, both unsigned int and box are 32 bits. Through this union, st_box and ui_box share a block of memory. Which bit in the bit field corresponds to which bit in the unsigned int depends on the compiler and hardware.
Using the union to zero the bit field, the code is as follows:
1 | union u_box u; |
extern
C++与C编译区别
在C中常在头文件见到extern "C"修饰函数,那有什么作用呢? 是用于C链接在C语言模块中定义的函数。
C虽然兼容C,但C文件中函数编译后生成的符号与C语言生成的不同。因为C支持函数重载,C函数编译后生成的符号带有函数参数类型的信息,而C则没有。
例如int add(int a, int b)函数经过C编译器生成.o文件后,add会变成形如add_int_int之类的, 而C的话则会是形如_add, 就是说:相同的函数,在C和C中,编译后生成的符号不同。
这就导致一个问题:如果C中使用C语言实现的函数,在编译链接的时候,会出错,提示找不到对应的符号。此时extern "C"就起作用了:告诉链接器去寻找_add这类的C语言符号,而不是经过C修饰的符号。
C++调用C函数
C++调用C函数的例子: 引用C的头文件时,需要加extern "C"
Study reference: http://www.yuan-ji.me/C-C-位域-Bit-fields-学习心得/
extern
Difference between C++ and C compilation
In C++, you often see functions modified with extern “C” in header files. What is the purpose? It is used for C++ to link functions defined in C language modules.
Although C++ is compatible with C, the symbols generated after compiling functions in C++ files differ from those generated in C. Because C++ supports function overloading, the symbols generated after compiling C++ functions carry information about the function parameter types, while C does not.
For example, after the int add(int a, int b) function is compiled into a .o file by the C++ compiler, add becomes something like add_int_int; in C it would be something like _add. That is to say: the same function generates different symbols after compilation in C and C++.
This leads to a problem: if C++ uses a function implemented in C, an error will occur during compilation and linking, saying that the corresponding symbol cannot be found. This is where extern "C" comes into play: it tells the linker to look for C language symbols like _add, rather than symbols mangled by C++.
C++ calling C functions
Example of C++ calling C functions: when referencing C header files, you need to add extern "C"
1 | //add.h |
编译:
Compile:
1 | //Generate add.o file |
链接:
Link:
1 | g++ add.cpp add.o -o main |
没有添加extern “C” 报错:
Error without adding extern “C”:
1 | > g++ add.cpp add.o -o main |
添加extern "C"后:
add.cpp
After adding extern “C”:
add.cpp
1 |
|
编译的时候一定要注意,先通过gcc生成中间文件add.o。
When compiling, be sure to first generate the intermediate file add.o with gcc.
1 | gcc -c add.c |
然后编译:
Then compile:
1 | g++ add.cpp add.o -o main |
而通常为了C代码能够通用,即既能被C调用,又能被C++调用,头文件通常会有如下写法:
And usually, to make C code universal — callable from both C and C++ — the header file usually has the following form:
1 |
|
即在C调用该接口时,会以C接口的方式调用。这种方式使得C者不需要额外的extern C,而标准库头文件通常也是类似的做法,否则你为何不需要extern C就可以直接使用stdio.h中的C函数呢?
C中调用C++函数
extern "C"在C中是语法错误,需要放在C++头文件中。
That is, when C++ calls this interface, it is called in the C interface way. This way, C++ users don’t need an extra extern “C”, and standard library header files usually do the same; otherwise, why could you use C functions from stdio.h directly without extern “C”?
C calling C++ functions
extern "C" is a syntax error in C, so it needs to be placed in C++ header files.
1 | // add.h |
编译:
Compile:
1 | g++ -c add.cpp |
链接:
Link:
1 | gcc add.c add.o -o main |
上述案例源代码见:
综上,总结出使用方法,在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。所以使用extern "C"全部都放在于cpp程序相关文件或其头文件中。
总结出如下形式:
(1)C++调用C函数:
See the source code of the above case at:
In summary, the usage method is: in C language header files, external functions can only be specified as extern type. The C language does not support extern “C” declarations, and including extern “C” in a .c file causes a compilation syntax error. So extern “C” should all be placed in files related to cpp programs or their header files.
The forms are summarized as follows:
(1) C++ calling C functions:
1 | //xx.h |
(2)C调用C++函数
(2) C calling C++ functions
1 | //xx.h |
不过与C调用C接口不同,C确实是能够调用编译好的C函数,而这里C调用C++,不过是把C代码当成C代码编译后调用而已。也就是说,C并不能直接调用C库函数。
struct
C中struct
- 在C中struct只单纯的用作数据的复合类型,也就是说,在结构体声明中只能将数据成员放在里面,而不能将函数放在里面。
- 在C结构体声明中不能使用C访问修饰符,如:public、protected、private 而在C中可以使用。
- 在C中定义结构体变量,如果使用了下面定义必须加struct。
- C的结构体不能继承(没有这一概念)。
- 若结构体的名字与函数名相同,可以正常运行且正常的调用!例如:可以定义与 struct Base 不冲突的 void Base() {}。
完整案例:
However, unlike C++ calling C interfaces, C++ can indeed call compiled C functions, while C calling C++ here is just compiling C++ code as C code and then calling it. In other words, C cannot directly call C++ library functions.
struct
struct in C
- In C, struct is only used as a composite data type, meaning that only data members can be placed in a struct declaration, and functions cannot be placed inside.
- C++ access modifiers such as public, protected, and private cannot be used in C struct declarations, but they can be used in C++.
- In C, when defining a struct variable, if the following definition is used, struct must be added.
- C structs cannot inherit (this concept does not exist).
- If the name of a struct is the same as a function name, it can run and be called normally! For example, you can define void Base() {} without conflicting with struct Base.
Complete example:
1 |
|
最后输出:
Final output:
1 | 1 |
C++中struct
与C对比如下:
- C++结构体中不仅可以定义数据,还可以定义函数。
- C++结构体中可以使用访问修饰符,如:public、protected、private 。
- C++结构体使用可以直接使用不带struct。
- C++继承
- 若结构体的名字与函数名相同,可以正常运行且正常的调用!但是定义结构体变量时候只用用带struct的!
例如:
情形1:不适用typedef定义结构体别名
未添加同名函数前:
struct in C++
Compared with C:
- In C++ structs, not only data but also functions can be defined.
- Access modifiers such as public, protected, and private can be used in C++ structs.
- C++ structs can be used directly without struct.
- C++ inheritance
- If the name of a struct is the same as a function name, it can run and be called normally! But when defining a struct variable, you must use struct!
For example:
Case 1: not using typedef to define a struct alias
Before adding the function with the same name:
1 | struct Student { |
添加同名函数后:
After adding the function with the same name:
1 | struct Student { |
情形二:使用typedef定义结构体别名
Case 2: using typedef to define a struct alias
1 | typedef struct Base1 { |
前三种案例
The first three cases
1 |
|
继承案例
Inheritance case
1 |
|
同名函数
Same-name function
1 |
|
总结
C和C++中的Struct区别
| C | C++ |
|---|---|
| 不能将函数放在结构体声明 | 能将函数放在结构体声明 |
| 在C结构体声明中不能使用C++访问修饰符。 | public、protected、private 在C++中可以使用。 |
| 在C中定义结构体变量,如果使用了下面定义必须加struct。 | 可以不加struct |
| 结构体不能继承(没有这一概念)。 | 可以继承 |
| 若结构体的名字与函数名相同,可以正常运行且正常的调用! | 若结构体的名字与函数名相同,使用结构体,只能使用带struct定义! |
struct 与 class
总的来说,struct 更适合看成是一个数据结构的实现体,class 更适合看成是一个对象的实现体。
区别:
最本质的一个区别就是默认的访问控制
默认的继承访问权限。struct 是 public 的,class 是 private 的。
struct 作为数据结构的实现体,它默认的数据访问控制是 public 的,而 class 作为对象的实现体,它默认的成员变量访问控制是 private 的。
union
联合(union)是一种节省空间的特殊的类,一个 union 可以有多个数据成员,但是在任意时刻只有一个数据成员可以有值。当某个成员被赋值后其他成员变为未定义状态。联合有如下特点:
- 默认访问控制符为 public
- 可以含有构造函数、析构函数
- 不能含有引用类型的成员
- 不能继承自其他类,不能作为基类
- 不能含有虚函数
- 匿名 union 在定义所在作用域可直接访问 union 成员
- 匿名 union 不能包含 protected 成员或 private 成员
- 全局匿名联合必须是静态(static)的
Summary
Differences between struct in C and C++
| C | C++ |
|---|---|
| Functions cannot be placed in struct declarations | Functions can be placed in struct declarations |
| C++ access modifiers cannot be used in C struct declarations. | public, protected, private can be used in C++. |
| In C, when defining a struct variable with the definition below, struct must be added. | struct can be omitted |
| Structs cannot inherit (this concept does not exist). | Can inherit |
| If the name of a struct is the same as a function name, it can run and be called normally! | If the name of a struct is the same as a function name, when using the struct, only the definition with struct can be used! |
struct vs class
In general, struct is more suitable as an implementation of a data structure, while class is more suitable as an implementation of an object.
Differences:
The most essential difference is the default access control
The default inheritance access permission. struct is public, class is private.
As an implementation of a data structure, struct’s default data access control is public, while as an implementation of an object, class’s default member variable access control is private.
union
A union is a special class that saves space. A union can have multiple data members, but at any moment only one data member can have a value. When a member is assigned, other members become undefined. A union has the following characteristics:
- The default access specifier is public
- Can contain constructors and destructors
- Cannot contain reference type members
- Cannot inherit from other classes, cannot be used as a base class
- Cannot contain virtual functions
- Members of an anonymous union can be accessed directly in the scope where the union is defined
- Anonymous unions cannot contain protected or private members
- Global anonymous unions must be static
1 |
|
C 实现 C++ 多态
C++实现案例
C中的多态:在C中会维护一张虚函数表,根据赋值兼容规则,我们知道父类的指针或者引用是可以指向子类对象的。
如果一个父类的指针或者引用调用父类的虚函数则该父类的指针会在自己的虚函数表中查找自己的函数地址,如果该父类对象的指针或者引用指向的是子类的对象,而且该子类已经重写了父类的虚函数,则该指针会调用子类的已经重写的虚函数。
Implementing C++ polymorphism in C
C++ implementation example
Polymorphism in C++: C++ maintains a virtual function table. According to the assignment compatibility rule, we know that a parent class pointer or reference can point to a child class object.
If a parent class pointer or reference calls a parent class virtual function, the parent class pointer will look up its own function address in its own virtual function table. If the pointer or reference of the parent class object points to a child class object, and the child class has overridden the parent’s virtual function, then the pointer will call the child class’s overridden virtual function.
1 |
|
C实现
- 封装
C语言中是没有class类这个概念的,但是有struct结构体,我们可以考虑使用struct来模拟;
使用函数指针把属性与方法封装到结构体中。
- 继承
结构体嵌套
- 多态
类与子类方法的函数指针不同
在C语言的结构体内部是没有成员函数的,如果实现这个父结构体和子结构体共有的函数呢?我们可以考虑使用函数指针来模拟。但是这样处理存在一个缺陷就是:父子各自的函数指针之间指向的不是类似C++中维护的虚函数表而是一块物理内存,如果模拟的函数过多的话就会不容易维护了。
模拟多态,必须保持函数指针变量对齐(在内容上完全一致,而且变量对齐上也完全一致)。否则父类指针指向子类对象,运行崩溃!
Implementation in C
- Encapsulation
The C language has no concept of class, but it has struct. We can consider using struct to simulate it;
Use function pointers to encapsulate properties and methods into a struct.
- Inheritance
Struct nesting
- Polymorphism
The function pointers of the parent class and the child class methods are different
There are no member functions inside a struct in C. How do we implement functions shared by the parent struct and the child struct? We can consider using function pointers to simulate it. But this approach has a drawback: the function pointers of the parent and child don’t point to a virtual function table like in C++, but to a block of physical memory. If too many functions are simulated, it becomes hard to maintain.
To simulate polymorphism, the function pointer variables must be aligned (identical in content and also in variable alignment). Otherwise, if a parent class pointer points to a child class object, it will crash at runtime!
1 |
|
explicit
- When explicit modifies a constructor, it prevents implicit conversion and copy initialization
- When explicit modifies a conversion function, it prevents implicit conversion, except for contextual conversions
1 |
|
参考链接:
https://stackoverflow.com/questions/4600295/what-is-the-meaning-of-operator-bool-const
friend
概述
友元提供了一种 普通函数或者类成员函数 访问另一个类中的私有或保护成员 的机制。也就是说有两种形式的友元:
(1)友元函数:普通函数对一个访问某个类中的私有或保护成员。
(2)友元类:类A中的成员函数访问类B中的私有或保护成员
优点:提高了程序的运行效率。
缺点:破坏了类的封装性和数据的透明性。
总结:
- 能访问私有成员
- 破坏封装性
- 友元关系不可传递
- 友元关系的单向性
- 友元声明的形式及数量不受限制
友元函数
在类声明的任何区域中声明,而定义则在类的外部。
Reference link:
https://stackoverflow.com/questions/4600295/what-is-the-meaning-of-operator-bool-const
friend
Overview
Friend provides a mechanism for ordinary functions or class member functions to access private or protected members in another class. That is, there are two forms of friend:
(1) Friend function: an ordinary function accesses private or protected members in a certain class.
(2) Friend class: a member function of class A accesses private or protected members of class B
Advantages: improves the running efficiency of the program.
Disadvantages: destroys the encapsulation of the class and the transparency of the data.
Summary:
- Can access private members
- Destroys encapsulation
- Friend relationships are not transitive
- Friend relationships are one-way
- There is no limit on the form and number of friend declarations
Friend functions
Declared in any region of the class declaration, while defined outside the class.
1 | friend <类型><友元函数名>(<参数表>); |
注意,友元函数只是一个普通函数,并不是该类的类成员函数,它可以在任何地方调用,友元函数中通过对象名来访问该类的私有或保护成员。
Note that a friend function is just an ordinary function, not a member function of the class. It can be called anywhere. Inside a friend function, the private or protected members of the class are accessed through the object name.
1 |
|
Friend classes
The friend class is declared in the class’s declaration, while its implementation is outside the class.
1 | friend class <友元类名>; |
类B是类A的友元,那么类B可以直接访问A的私有成员。
If class B is a friend of class A, then class B can directly access A’s private members.
1 |
|
注意
-
友元关系没有继承性
假如类B是类A的友元,类C继承于类A,那么友元类B是没办法直接访问类C的私有或保护成员。 -
友元关系没有传递性
假如类B是类A的友元,类C是类B的友元,那么友元类C是没办法直接访问类A的私有或保护成员,也就是不存在“友元的友元”这种关系。
using
基本使用
局部与全局using,具体操作与使用见下面案例:
Notes
-
Friend relationships are not inherited
If class B is a friend of class A, and class C inherits from class A, then friend class B cannot directly access class C’s private or protected members. -
Friend relationships are not transitive
If class B is a friend of class A, and class C is a friend of class B, then friend class C cannot directly access class A’s private or protected members. That is, there is no such relationship as a “friend of a friend”.
using
Basic usage
For local and global using, see the following case for the specific operations and usage:
1 |
|
1 | class Base{ |
类Derived私有继承了Base,对于它来说成员变量n和成员函数size都是私有的,如果使用了using语句,可以改变他们的可访问性,如上述例子中,size可以按public的权限访问,n可以按protected的权限访问。
Class Derived privately inherits from Base, so for it, the member variable n and the member function size are both private. If a using declaration is used, their accessibility can be changed. In the example above, size can be accessed with public permission, and n can be accessed with protected permission.
1 |
|
函数重载
在继承过程中,派生类可以覆盖重载函数的0个或多个实例,一旦定义了一个重载版本,那么其他的重载版本都会变为不可见。
如果对于基类的重载函数,我们需要在派生类中修改一个,又要让其他的保持可见,必须要重载所有版本,这样十分的繁琐。
Function overloading
During inheritance, a derived class can override zero or more instances of an overloaded function. Once an overloaded version is defined, all other overloaded versions become invisible.
If for the base class’s overloaded functions, we need to modify one in the derived class while keeping the others visible, we must overload all versions, which is very tedious.
1 |
|
如上代码中,在派生类中使用using声明语句指定一个名字而不指定形参列表,所以一条基类成员函数的using声明语句就可以把该函数的所有重载实例添加到派生类的作用域中。此时,派生类只需要定义其特有的函数就行了,而无需为继承而来的其他函数重新定义。
取代typedef
C中常用typedef A B这样的语法,将B定义为A类型,也就是给A类型一个别名B
对应typedef A B,使用using B=A可以进行同样的操作。
In the code above, a using declaration in the derived class specifies a name without a parameter list, so a single using declaration of a base class member function can add all overloaded instances of that function to the derived class’s scope. At this point, the derived class only needs to define its own special functions, without redefining other functions inherited from the base class.
Replacing typedef
In C, the syntax typedef A B is commonly used to define B as type A, that is, to give type A an alias B.
Corresponding to typedef A B, using B=A can do the same operation.
1 |
|
: :
- 全局作用域符(::name):用于类型名称(类、类成员、成员函数、变量等)前,表示作用域为全局命名空间
- 类作用域符(class::name):用于表示指定类型的作用域范围是具体某个类的
- 命名空间作用域符(namespace::name):用于表示指定类型的作用域范围是具体某个命名空间的
: :
- Global scope operator (::name): used before a type name (class, class member, member function, variable, etc.), indicating that the scope is the global namespace
- Class scope operator (class::name): used to indicate that the scope of the specified type is a specific class
- Namespace scope operator (namespace::name): used to indicate that the scope of the specified type is a specific namespace
1 |
|
enum
Traditional behavior
Enums have the following problems:
- The scope is not restricted, which can easily cause naming conflicts. For example, the following cannot compile:
1 |
|
- 会隐式转换为int
- 用来表征枚举变量的实际类型不能明确指定,从而无法支持枚举类型的前向声明。
经典做法
解决作用域不受限带来的命名冲突问题的一个简单方法是,给枚举变量命名时加前缀,如上面例子改成 COLOR_BLUE 以及 FEELING_BLUE。
一般说来,为了一致性我们会把所有常量统一加上前缀。但是这样定义枚举变量的代码就显得累赘。C 程序中可能不得不这样做。不过 C++ 程序员恐怕都不喜欢这种方法。替代方案是命名空间:
- It is implicitly converted to int
- The actual type used to represent an enum variable cannot be explicitly specified, so forward declaration of enum types is not supported.
Classic approach
A simple way to solve the naming conflict problem caused by unrestricted scope is to add a prefix when naming enum variables, such as changing the above examples to COLOR_BLUE and FEELING_BLUE.
Generally speaking, for consistency we would add a prefix to all constants. But this makes the code that defines enum variables cumbersome. C programs may have to do this. However, C++ programmers probably don’t like this approach. An alternative is namespaces:
1 | namespace Color |
这样之后就可以用 Color::Type c = Color::RED; 来定义新的枚举变量了。如果 using namespace Color 后,前缀还可以省去,使得代码简化。不过,因为命名空间是可以随后被扩充内容的,所以它提供的作用域封闭性不高。在大项目中,还是有可能不同人给不同的东西起同样的枚举类型名。
更“有效”的办法是用一个类或结构体来限定其作用域,例如:定义新变量的方法和上面命名空间的相同。不过这样就不用担心类在别处被修改内容。这里用结构体而非类,是因为本身希望这些常量可以公开访问。
After this, you can use Color::Type c = Color::RED; to define a new enum variable. If using namespace Color is added, the prefix can be omitted, simplifying the code. However, since a namespace can be expanded with more content later, the scope closure it provides is not strong. In large projects, different people may still give the same enum type names to different things.
A more “effective” way is to use a class or struct to restrict its scope. For example, the way to define new variables is the same as with namespaces above. But then you don’t have to worry about the class content being modified elsewhere. A struct is used here instead of a class, because we want these constants to be publicly accessible.
1 | struct Color1 |
C++11 的枚举类
上面的做法解决了第一个问题,但对于后两个仍无能为力。庆幸的是,C++11 标准中引入了“枚举类”(enum class),可以较好地解决上述问题。
- 新的enum的作用域不在是全局的
- 不能隐式转换成其他类型
Enum classes in C++11
The above approach solves the first problem, but it still can’t do anything about the latter two. Fortunately, the C++11 standard introduced “enum class”, which can solve the above problems well.
- The scope of the new enum is no longer global
- Cannot be implicitly converted to other types
1 | /** |
- 可以指定用特定的类型来存储enum
- The specific type used to store the enum can be specified
1 | enum class Color3:char; // 前向声明 |
具体实现见:
See the specific implementation at:
1 |
|
类中的枚举类型
有时我们希望某些常量只在类中有效。 由于#define 定义的宏常量是全局的,不能达到目的,于是想到实用const 修饰数据成员来实现。而const 数据成员的确是存在的,但其含义却不是我们所期望的。
const 数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的,因为类可以创建多个对象,不同的对象其 const 数据成员的值可以不同。
不能在类声明中初始化 const 数据成员。以下用法是错误的,因为类的对象未被创建时,编译器不知道 SIZE 的值是什么。(c++11标准前)
Enum types in classes
Sometimes we want certain constants to be valid only within a class. Since macro constants defined with #define are global, they can’t achieve this goal, so we think of using const to modify data members. const data members do exist, but their meaning is not what we expect.
A const data member is constant only during the lifetime of an object; for the entire class it is mutable, because a class can create multiple objects, and different objects can have different values for their const data members.
Const data members cannot be initialized in the class declaration. The following usage is wrong, because when the class’s object has not been created, the compiler doesn’t know what the value of SIZE is. (Before the C++11 standard)
1 | class A |
正确应该在类的构造函数的初始化列表中进行:
The correct way is to do it in the constructor’s initializer list:
1 | class A |
怎样才能建立在整个类中都恒定的常量呢?
别指望 const 数据成员了,应该用类中的枚举常量来实现。例如:
How can we create constants that are constant throughout the entire class?
Don’t count on const data members; you should use enum constants in the class. For example:
1 | class Person{ |
Enum constants do not occupy object storage space; they are all evaluated at compile time.
The disadvantage of enum constants is: their implicit data type is integer, their maximum value is limited, and they cannot represent floating point.
decltype
Basic usage
The syntax of decltype is:
1 | decltype (expression) |
这里的括号是必不可少的,decltype的作用是“查询表达式的类型”,因此,上面语句的效果是,返回 expression 表达式的类型。注意,decltype 仅仅“查询”表达式的类型,并不会对表达式进行“求值”。
推导出表达式类型
The parentheses here are essential. The function of decltype is to “query the type of an expression”, so the effect of the above statement is to return the type of the expression. Note that decltype only “queries” the type of the expression and does not “evaluate” the expression.
Deduce the type of an expression
1 | int i = 4; |
1 | using size_t = decltype(sizeof(0));//sizeof(a)的返回值为size_t类型 |
This, like auto, also improves code readability.
Reusing anonymous types
In C++, we sometimes encounter anonymous types, such as:
1 | struct |
而借助decltype,我们可以重新使用这个匿名的结构体:
With the help of decltype, we can reuse this anonymous struct:
1 | decltype(anon_s) as ;//定义了一个上面匿名的结构体 |
Combining with auto in generic programming, for tracking the return type of functions
This is also the greatest use of decltype.
1 | template <typename T> |
完整代码见:
See the complete code at:
1 |
|
判别规则
对于decltype(e)而言,其判别结果受以下条件的影响:
如果e是一个没有带括号的标记符表达式或者类成员访问表达式,那么的decltype(e)就是e所命名的实体的类型。此外,如果e是一个被重载的函数,则会导致编译错误。
否则 ,假设e的类型是T,如果e是一个将亡值,那么decltype(e)为T&&
否则,假设e的类型是T,如果e是一个左值,那么decltype(e)为T&。
否则,假设e的类型是T,则decltype(e)为T。
标记符指的是除去关键字、字面量等编译器需要使用的标记之外的程序员自己定义的标记,而单个标记符对应的表达式即为标记符表达式。例如:
Discriminating rules
For decltype(e), its result is affected by the following conditions:
If e is an unparenthesized id-expression or a class member access expression, then decltype(e) is the type of the entity named by e. In addition, if e is an overloaded function, it will cause a compilation error.
Otherwise, assuming the type of e is T, if e is an xvalue, then decltype(e) is T&&
Otherwise, assuming the type of e is T, if e is an lvalue, then decltype(e) is T&.
Otherwise, assuming the type of e is T, then decltype(e) is T.
An identifier refers to tokens defined by the programmer himself, excluding keywords, literals, and other tokens that the compiler needs to use, and the expression corresponding to a single identifier is an id-expression. For example:
1 | int arr[4] |
则arr为一个标记符表达式,而arr[3]+0不是。
举例如下:
Then arr is an id-expression, while arr[3]+0 is not.
Examples are as follows:
1 | int i = 4; |
学习参考:https://www.cnblogs.com/QG-whz/p/4952980.html
引用和指针
引用与指针
总论:
| 引用 | 指针 |
|---|---|
| 必须初始化 | 可以不初始化 |
| 不能为空 | 可以为空 |
| 不能更换目标 | 可以更换目标 |
引用必须初始化,而指针可以不初始化。
我们在定义一个引用的时候必须为其指定一个初始值,但是指针却不需要。
Study reference: https://www.cnblogs.com/QG-whz/p/4952980.html
References and Pointers
References vs. Pointers
Overview:
| Reference | Pointer |
|---|---|
| Must be initialized | Can be left uninitialized |
| Cannot be null | Can be null |
| Cannot change target | Can change target |
A reference must be initialized, while a pointer can be left uninitialized.
When we define a reference, we must specify an initial value for it, but a pointer does not need one.
1 | int &r; //不合法,没有初始化引用 |
引用不能为空,而指针可以为空。
由于引用不能为空,所以我们在使用引用的时候不需要测试其合法性,而在使用指针的时候需要首先判断指针是否为空指针,否则可能会引起程序崩溃。
A reference cannot be null, while a pointer can be null.
Since a reference cannot be null, we don’t need to test its validity when using it, while when using a pointer, we need to first check whether the pointer is a null pointer, otherwise the program may crash.
1 | void test_p(int* p) |
引用不能更换目标
指针可以随时改变指向,但是引用只能指向初始化时指向的对象,无法改变。
A reference cannot change its target
A pointer can change what it points to at any time, but a reference can only point to the object it pointed to at initialization and cannot change.
1 | int a = 1; |
引用
左值引用
常规引用,一般表示对象的身份。
右值引用
右值引用就是必须绑定到右值(一个临时对象、将要销毁的对象)的引用,一般表示对象的值。
右值引用可实现转移语义(Move Sementics)和精确传递(Perfect Forwarding),它的主要目的有两个方面:
- 消除两个对象交互时不必要的对象拷贝,节省运算存储资源,提高效率。
- 能够更简洁明确地定义泛型函数。
引用折叠
X& &、X& &&、X&& &可折叠成X&X&& &&可折叠成X&&
C++的引用在减少了程序员自由度的同时提升了内存操作的安全性和语义的优美性。比如引用强制要求必须初始化,可以让我们在使用引用的时候不用再去判断引用是否为空,让代码更加简洁优美,避免了指针满天飞的情形。除了这种场景之外引用还用于如下两个场景:
引用型参数
一般我们使用const reference参数作为只读形参,这种情况下既可以避免参数拷贝还可以获得与传值参数一样的调用方式。
References
Lvalue references
Ordinary references, generally representing the identity of an object.
Rvalue references
An rvalue reference is a reference that must be bound to an rvalue (a temporary object, an object about to be destroyed), generally representing the value of an object.
Rvalue references enable move semantics and perfect forwarding. Their main purposes are two-fold:
- Eliminate unnecessary object copies when two objects interact, saving computational and storage resources and improving efficiency.
- Allow generic functions to be defined more concisely and clearly.
Reference collapsing
X& &,X& &&,X&& &can collapse toX&X&& &&can collapse toX&&
C++ references reduce the programmer’s freedom while improving the safety of memory operations and the elegance of semantics. For example, references require mandatory initialization, so when using a reference we don’t need to check whether it is null, making the code more concise and elegant and avoiding the situation of pointers flying everywhere. Besides this scenario, references are also used in the following two scenarios:
Reference parameters
Generally we use const reference parameters as read-only formal parameters. In this case, we can both avoid parameter copying and get the same calling style as pass-by-value parameters.
1 | void test(const vector<int> &data) |
引用型返回值
C++提供了重载运算符的功能,我们在重载某些操作符的时候,使用引用型返回值可以获得跟该操作符原来语法相同的调用方式,保持了操作符语义的一致性。一个例子就是operator []操作符,这个操作符一般需要返回一个引用对象,才能正确的被修改。
Reference return values
C++ provides operator overloading. When we overload certain operators, using a reference return value gives us the same calling style as the operator’s original syntax, keeping the operator’s semantics consistent. One example is the operator [] operator, which generally needs to return a reference object so that it can be modified correctly.
1 | vector<int> v(10); |
Performance gap between pointers and references
Is there a performance gap between pointers and references? Such a question requires going down to the assembly level to look at it. Let’s first write a test1 function that passes parameters by pointer:
1 | void test1(int* p) |
该代码段对应的汇编代码如下:
The assembly code corresponding to this code segment is as follows:
1 | (gdb) disassemble |
上述代码1、2行是参数调用保存现场操作;第3行是参数传递,函数调用第一个参数一般放在rdi寄存器,此行代码把rdi寄存器值(指针p的值)写入栈中;第4行是把栈中p的值写入rax寄存器;第5行是把立即数3写入到rax寄存器值所指向的内存中,此处要注意(%rax)两边的括号,这个括号并并不是可有可无的,(%rax)和%rax完全是两种意义,(%rax)代表rax寄存器中值所代表地址部分的内存,即相当于C代码中的*p,而%rax代表rax寄存器,相当于C代码中的p值,所以汇编这里使用了(%rax)而不是%rax。
我们再写出参数传递使用引用的C++代码段test2:
In the code above, lines 1 and 2 are the save-context operations for the parameter call; line 3 is the parameter passing — the first parameter of a function call is usually placed in the rdi register, and this line writes the rdi register value (the value of pointer p) to the stack; line 4 writes the value of p on the stack into the rax register; line 5 writes the immediate value 3 into the memory pointed to by the value of the rax register. Note the parentheses on both sides of (%rax) here — they are not optional. (%rax) and %rax have completely different meanings: (%rax) represents the memory at the address represented by the value in the rax register, i.e., equivalent to *p in C++ code, while %rax represents the rax register, equivalent to the value p in C++ code. So the assembly here uses (%rax) instead of %rax.
1 | void test2(int& r) |
这段代码对应的汇编代码如下:
The assembly code corresponding to this code is as follows:
1 | (gdb) disassemble |
我们发现test2对应的汇编代码和test1对应的汇编代码完全相同,这说明C编译器在编译程序的时候将指针和引用编译成了完全一样的机器码。所以C中的引用只是C对指针操作的一个“语法糖”,在底层实现时C编译器实现这两种操作的方法完全相同。
总结
C++中引入了引用操作,在对引用的使用加了更多限制条件的情况下,保证了引用使用的安全性和便捷性,还可以保持代码的优雅性。在适合的情况使用适合的操作,引用的使用可以一定程度避免“指针满天飞”的情况,对于提升程序稳定性也有一定的积极意义。最后,指针与引用底层实现都是一样的,不用担心两者的性能差距。
上述部分参考自:http://irootlee.com/juicer_pointer_reference/#
宏
宏中包含特殊符号
分为几种:#,##,\
字符串化操作符(#)
在一个宏中的参数前面使用一个#,预处理器会把这个参数转换为一个字符数组,换言之就是:#是“字符串化”的意思,出现在宏定义中的#是把跟在后面的参数转换成一个字符串。
注意:其只能用于有传入参数的宏定义中,且必须置于宏定义体中的参数名前。
例如:
We find that the assembly code corresponding to test2 is exactly the same as the assembly code corresponding to test1, which shows that the C++ compiler compiles pointers and references into exactly the same machine code when compiling the program. So a reference in C++ is just a “syntactic sugar” for pointer operations; at the underlying implementation level, the C++ compiler implements these two operations in exactly the same way.
Summary
C++ introduces reference operations. While adding more restrictions to the use of references, it ensures the safety and convenience of reference usage and can also keep the code elegant. Use the right operation in the right situation. Using references can, to some extent, avoid the situation of “pointers flying everywhere”, and it also has positive significance for improving program stability. Finally, pointers and references have the same underlying implementation, so there is no need to worry about a performance gap between the two.
Part of the above is referenced from: http://irootlee.com/juicer_pointer_reference/#
Macros
Special symbols in macros
Divided into several kinds: #, ##, \
Stringizing operator (#)
Putting a # before a parameter in a macro makes the preprocessor convert this parameter into a character array. In other words: # means “stringizing”; the # appearing in a macro definition converts the parameter that follows it into a string.
Note: it can only be used in macro definitions that take parameters, and it must be placed before the parameter name in the macro definition body.
For example:
1 |
|
上述代码给出了基本的使用与空格处理规则,空格处理规则如下:
- 忽略传入参数名前面和后面的空格。
The above code shows the basic usage and the space handling rules. The space handling rules are as follows:
- Spaces before and after the passed parameter name are ignored.
1 | string str = exp2( bac ); |
输出:
Output:
1 | bac 3 |
- 当传入参数名间存在空格时,编译器将会自动连接各个子字符串,用每个子字符串之间以一个空格连接,忽略剩余空格。
- When there are spaces between the passed parameter names, the compiler will automatically concatenate the substrings, joining each substring with a single space, ignoring the remaining spaces.
1 | string str1 = exp2( asda bac ); |
输出:
Output:
1 | asda bac 8 |
符号连接操作符(##)
“##”是一种分隔连接方式,它的作用是先分隔,然后进行强制连接。将宏定义的多个形参转换成一个实际参数名。
注意事项:
(1)当用##连接形参时,##前后的空格可有可无。
(2)连接后的实际参数名,必须为实际存在的参数名或是编译器已知的宏定义。
(3)如果##后的参数本身也是一个宏的话,##会阻止这个宏的展开。
示例:
Token-pasting operator (##)
"##" is a way of separating and then forcibly concatenating. It converts multiple formal parameters of a macro definition into one actual parameter name.
Notes:
(1) When connecting formal parameters with ##, the spaces before and after ## are optional.
(2) The actual parameter name after concatenation must be an actually existing parameter name or a macro definition known to the compiler.
(3) If the parameter after ## is itself a macro, ## will prevent this macro from being expanded.
Example:
1 |
|
Line continuation operator ()
When a defined macro cannot be fully expressed in one line, you can use “” to indicate that the next line continues the definition of this macro.
Note: leave a space before .
1 |
|
上述代码见:
See the above code at:
1 |
|
1 |
|
这个宏被展开后就是:
After this macro is expanded, it becomes:
1 | if(a>0) |
本意是a>0执行f1 f2,而实际是f2每次都会执行,所以就错误了。
为了解决这种问题,在写代码的时候,通常可以采用{}块。
如:
The intent is that when a>0, f1 and f2 are executed, but in reality f2 is executed every time, so it’s wrong.
To solve this problem, when writing code, you can usually use a {} block.
Like:
1 |
|
但是会发现上述宏展开后多了一个分号,实际语法不太对。(虽然编译运行没问题,正常没分号)。
避免使用goto控制流
在一些函数中,我们可能需要在return语句之前做一些清理工作,比如释放在函数开始处由malloc申请的内存空间,使用goto总是一种简单的方法:
But you’ll find that after expansion, the macro above has an extra semicolon, which is not syntactically correct. (Although it compiles and runs fine, normally there should be no semicolon).
Avoiding goto control flow
In some functions, we may need to do some cleanup before the return statement, such as releasing the memory space allocated by malloc at the beginning of the function. Using goto is always a simple way:
1 | int f() { |
但由于goto不符合软件工程的结构化,而且有可能使得代码难懂,所以很多人都不倡导使用,这个时候我们可以使用do{…}while(0)来做同样的事情:
But because goto does not conform to structured programming in software engineering and may make the code hard to understand, many people don’t advocate using it. At this point we can use do{…}while(0) to do the same thing:
1 | int ff() { |
这里将函数主体部分使用do{…}while(0)包含起来,使用break来代替goto,后续的清理工作在while之后,现在既能达到同样的效果,而且代码的可读性、可维护性都要比上面的goto代码好的多了。
避免由宏引起的警告
内核中由于不同架构的限制,很多时候会用到空宏,。在编译的时候,这些空宏会给出warning,为了避免这样的warning,我们可以使用do{…}while(0)来定义空宏:
Here the main body of the function is wrapped in do{…}while(0), using break instead of goto, and the subsequent cleanup work is done after the while. Now it achieves the same effect, and the code’s readability and maintainability are much better than the goto code above.
Avoiding warnings caused by macros
In the kernel, due to the limitations of different architectures, empty macros are often used. When compiling, these empty macros will produce warnings. To avoid such warnings, we can use do{…}while(0) to define empty macros:
1 | #define EMPTYMICRO do{}while(0) |
定义单一的函数块来完成复杂的操作
如果你有一个复杂的函数,变量很多,而且你不想要增加新的函数,可以使用do{…}while(0),将你的代码写在里面,里面可以定义变量而不用考虑变量名会同函数之前或者之后的重复。
这种情况应该是指一个变量多处使用(但每处的意义还不同),我们可以在每个do-while中缩小作用域,比如:
Define a single function block to complete complex operations
If you have a complex function with many variables, and you don’t want to add new functions, you can use do{…}while(0) and write your code inside it. Inside, you can define variables without worrying about name conflicts with variables before or after the function.
This situation refers to a variable being used in multiple places (but with different meanings in each place). We can narrow the scope in each do-while, for example:
1 | int fc() |
上述代码见:
See the above code at:
1 |
|
Study article: https://www.cnblogs.com/lizhenghn/p/3674430.html


