多线程学习笔记
开一篇多线程学习笔记,记录下在实习过程中遇到的一些简单问题。
注意:这是一篇以学习笔记,难免有误,主要写给自己参考。请酌情判别,如有错误,也欢迎指正!
开一篇多线程学习笔记,记录下在实习过程中遇到的一些简单问题。
注意:这是一篇以学习笔记,难免有误,主要写给自己参考。请酌情判别,如有错误,也欢迎指正!
诚如是,Life is too short to learn c++. 此篇记录一些我在学习 cpp 过程中遇到的一些知识点,仅作记录并梳理之效。里面可能会有大量参考其他网络博客,如有侵权,请联系我删除之。
__func__
: name of an function, exists in C99/C++11 (__FUNCTION__
is non standard)__LINE__
: line number of the code__FILE__
: filename of the file__DATE__
and __TIME__
: as you wish总结来说:基类部分在派生类部分之前被构造,当基类构造函数执行时派生类中的数据成员还没被初始化。如果基类构造函数中的虚函数调用被解析成调用派生类的虚函数,而派生类的虚函数中又访问到未初始化的派生类数据,将导致程序出现一些未定义行为和 bug。
ctor 应该设计的尽量简单,确保对象可以被正确构造。在 ctor 中调用本类的非静态成员都是不安全的,因为他们还没被构造,而有些成员是依赖对象的,而此时对象还没有被成功构造。
从存储空间角度:虚函数对应一个 vtable(虚函数表),这大家都知道,可是这个 vtable 其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable 来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到 vtable,所以构造函数不能是虚函数。
从使用角度:虚函数主要用于在信息不全的情况下,能使重载的函数得到对应的调用。构造函数本身就是要初始化实例,那使用虚函数也没有实际意义呀。所以构造函数没有必要是虚函数。 虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
构造函数不需要是虚函数,也不允许是虚函数,因为创建一个对象时我们总是要明确指定对象的类型,尽管我们可能通过实验室的基类的指针或引用去访问它。但析构却不一定,我们往往通过基类的指针来销毁对象。这时候如果析构函数不是虚函数,就不能正确识别对象类型从而不能正确调用析构函数。
—————————————————— 版权声明:本文为 CSDN 博主「cainiao000001」的原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/cainiao000001/article/details/81603782
https://zhuanlan.zhihu.com/p/60543586
C++ 规定了虚函数的行为,但将实现方法留给了编译器的作者。不需要知道实现方法也可以很好的使用虚函数,但了解虚函数的工作原理有助于更好地理解概念。
通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数地址数组的指针。
这种数组称为虚函数表(Virtual Function Table, vtbl)。
虚函数表是一个数组,数组的元素是指针,指针指的是虚函数的地址。
具有虚函数的类的实例,都会在头部存一个指向虚函数表的指针。
TYPE | Bytes |
---|---|
(unsigned) int | 4 |
(unsigned) short | 2 |
(unsigned) long | 8 |
float | 4 |
double | 8 |
long double | 16 |
(unsigned) char | 1 |
bool | 1 |
指针占几个字节 指针即为地址,指针几个字节跟语言无关,而是跟系统的寻址能力有关,譬如以前是 16 为地址,指针即为 2 个字节,现在一般是 32 位系统,所以是 4 个字节,以后 64 位,则就为 8 个字节。
NOTE: 类成员函数指针一般为普通指针的两倍大小。
literal 5.0
类型为double
,5.0f
类型为float
。不加f
后缀默认double
.
当一个类包含静态成员时,最好的做法是在类中声明,在类外初始化。由于静态成员是所有对象共享的,如果在类内初始化,则每个对象构造时,都要执行一遍静态成员的初始化,这无疑是一种浪费。
|
|
The destructor is called whenever an object’s lifetime ends, which includes
cf. https://en.cppreference.com/w/cpp/language/destructor
字面值常量 Cf. https://www.learncpp.com/cpp-tutorial/literals/
符号常量 Cf. https://www.learncpp.com/cpp-tutorial/const-constexpr-and-symbolic-constants/
Runtime vs compile-time constants
Runtime constants are constants whose initialization values can only be resolved at runtime (when your program is running). The following are examples of runtime constants:
|
|
Compile-time constants are constants whose initialization values can be determined at compile-time (when your program is compiling). The following are examples of compile-time constants:
|
|
Compile-time constants enable the compiler to perform optimizations that aren’t available with runtime constants. For example, whenever gravity is used, the compiler can simply substitute the identifier gravity with the literal double 9.8.
To help provide more specificity, C++11 introduced the keyword constexpr
, which ensures that a constant must be a compile-time constant.
Any variable that should not be modifiable after initialization and whose initializer is known at compile-time should be declared as
constexpr
.Any variable that should not be modifiable after initialization and whose initializer is not known at compile-time should be declared as
const
.
Note that literals are also implicitly constexpr, as the value of a literal is known at compile-time.
A constant expression is an expression that can be evaluated at compile-time. For example:
|
|
In the above program, because the literal values 3 and 4 are known at compile-time, the compiler can evaluate the expression 3 + 4 at compile-time and substitute in the resulting value 7. That makes the code faster because 3 + 4 no longer has to be calculated at runtime.
Constexpr variables can also be used in constant expressions:
|
|
In the above example, because x and y are constexpr, the expression x + y is a constant expression that can be evaluated at compile-time. Similar to the literal case, the compiler can substitute in the value 7.
Object-like macro has the form:
|
|
Whenever the preprocessor encounters this directive, any further occurrence of identifier is replaced by substitution_text. The identifier is traditionally typed in all capital letters, using underscores to represent spaces.
Avoid using #define to create symbolic constants macros. Use const or constexpr variables instead.
Macros can have naming conflicts with normal code. For example:
|
|
If someheader.h happened to #define a macro named beta, this simple program would break, as the preprocessor would replace the int variable beta’s name with whatever the macro’s value was. This is normally avoided by using all caps for macro names, but it can still happen.
C++ 规范在“结构”上使用了和 C 相同的,简单的内存布局原则:成员变量按其被声明的顺序排列,按具体实现所规定的对齐原则在内存地址上对齐。
|
|
Cf. https://www.learncpp.com/cpp-tutorial/object-sizes-and-the-sizeof-operator/#comment-563585
Cf. http://www.catb.org/esr/structure-packing/
从编译到函数模板的调用,编译器必须在非模板重载、模板重载和模板重载的特化间决定。
|
|
注意只有非模板和初等模板重载参与重载决议。特化不是重载,且不受考虑。只有在重载决议选择最佳匹配初等函数模板后,才检验其特化以查看何为最佳匹配。
|
|
即重载的优先级要高于特化。
关于模板函数重载的更多内容,参考 function_template。
Cf. https://www.learncpp.com/cpp-tutorial/introduction-to-the-preprocessor/
#include
When you #include a file, the preprocessor replaces the #include directive with the contents of the included file. The included contents are then preprocessed (along with the rest of the file), and then compiled.
The #define directive can be used to create a macro. In C++, a macro is a rule that defines how input text is converted into replacement output text.
There are two basic types of macros: object-like macros, and function-like macros. Object-like macros can be defined in one of two ways:
|
|
结论:宏展开在预编译指令 (Preprocessor directives) 无效。
|
|
Macros only cause text substitution for normal code. Other preprocessor commands are ignored. Consequently, the PRINT_JOE in #ifdef PRINT_JOE is left alone.
For example:
|
|
In actuality, the output of the preprocessor contains no directives at all – they are all resolved/stripped out before compilation, because the compiler wouldn’t know what to do with them.
Once the preprocessor has finished, all defined identifiers from that file are discarded. This means that directives are only valid from the point of definition to the end of the file in which they are defined. Directives defined in one code file do not have impact on other code files in the same project.
宏定义仅在本文件有效,一旦预编译阶段结束,所有宏都将失效。因为,预编译就是将所有的预编译指令都处理掉,该替换的替换(宏展开),该选择的选择,该丢弃的丢弃(条件编译),然后交给编译器去编译,谨记:编译器是读不懂预编译指令的!
Consider the following example:
function.cpp:
|
|
main.cpp:
|
|
The above program will print:
Not printing!
Even though PRINT was defined in main.cpp, that doesn’t have any impact on any of the code in function.cpp (PRINT is only #defined from the point of definition to the end of main.cpp). This will be of consequence when we discuss header guards in a future lesson.
Cf. https://www.learncpp.com/cpp-tutorial/header-files/
对于多文件项目,文件是单独编译的。要想调用一个自定义函数,linker 必须能找到这个函数在哪里定义。
|
|
上述文件是可以编译通过的,因为没有发生对add
的调用,所以 linker 不会去找add
的定义(当然如果要找也找不到)。
但是如果某处发起了对add
的调用(例如去掉注释),那么上述程序在 link 阶段会报错:
|
|
在多文件编程时,往往需要 forawrd declaration,这些前置声明必须在其他某个地方被定义且只被定义一次。这样,linker 才能正确的完成链接。任何重复定义或未定义都会在 link 阶段报错。
考虑如下例子:
add.cpp:
|
|
main.cpp:
|
|
在编译 main.cpp 的时候,因为有add
的前置声明,所以可以通过。但为了 link 的时候能够找到add
的定义,add.cpp 必须也被编译,所以正确的编译方式应该是:
|
|
从上面的论述我们隐约可见,在多文件编程中,我们可能会大量的使用前置声明(forward declaration),一旦文件多起来,这将非常枯燥。所以头文件的出现就是为了解决这个问题:把所有的声明放在一起。
Let’s write a header file to relieve us of this burden. Writing a header file is surprisingly easy, as header files only consist of two parts:
add.h:
|
|
main.cpp:
|
|
add.cpp:
|
|
When the preprocessor processes the #include "add.h"
line, it copies the contents of add.h into the current file at that point. Because our add.h contains a forward declaration for function add, that forward declaration will be copied into main.cpp. The end result is a program that is functionally the same as the one where we manually added the forward declaration at the top of main.cpp.
Consequently, our program will compile and link correctly.
如上图所示,会产生一个重复定义的错误。由于 add.h 中包含了函数定义,而非前置声明。编译 main.cpp 的时候,add.h 中的代码插入到 main.cpp 中,产生一次add
函数的定义。同理,编译 add.cpp 的时候也定义了一次add
函数。link 阶段会发生歧义,以致报错。
此时如果不编译 add.cpp 其实是可行的:
但谁又能保证只有一个文件#include "add.h"
呢?所以头文件中应该只包含声明,而不应该包含实现。
The primary purpose of a header file is to propagate declarations to code files.
Key insight: Header files allow us to put declarations in one location and then import them wherever we need them. This can save a lot of typing in multi-file programs.
Header files should generally not contain function and variable definitions, so as not to violate the one definition rule. An exception is made for symbolic constants (which we cover in lesson 4.15 – Symbolic constants: const and constexpr variables).
标准库自动链接
注意:clang 不会自动链接,需要手动链接
clang main.cpp -lstdc++
When it comes to functions and variables, it’s worth keeping in mind that header files typically only contain function and variable declarations, not function and variable definitions (otherwise a violation of the one definition rule could result). std::cout is forward declared in the iostream header, but defined as part of the C++ standard library, which is automatically linked into your program during the linker phase.
The #include order of header files
Cf. https://www.learncpp.com/cpp-tutorial/header-files/ for “the #inclue order of header files”.
Cf. https://www.learncpp.com/cpp-tutorial/introduction-to-fundamental-data-types/
The smallest unit of memory is a binary digit (also called a bit), which can hold a value of 0 or 1. You can think of a bit as being like a traditional light switch – either the light is off (0), or it is on (1). There is no in-between. If you were to look at a random segment of memory, all you would see is …011010100101010… or some combination thereof.
Memory is organized into sequential units called memory addresses (or addresses for short). Similar to how a street address can be used to find a given house on a street, the memory address allows us to find and access the contents of memory at a particular location.
Perhaps surprisingly, in modern computer architectures, each bit does not get its own unique memory address. This is because the number of memory addresses are limited, and the need to access data bit-by-bit is rare. Instead, each memory address holds 1 byte of data. A byte is a group of bits that are operated on as a unit. The modern standard is that a byte is comprised of 8 sequential bits.
Data types
Because all data on a computer is just a sequence of bits, we use a data type (often called a “type” for short) to tell the compiler how to interpret the contents of memory in some meaningful way. You have already seen one example of a data type: the integer. When we declare a variable as an integer, we are telling the compiler “the piece of memory that this variable uses is going to be interpreted as an integer value”.
When you give an object a value, the compiler and CPU take care of encoding your value into the appropriate sequence of bits for that data type, which are then stored in memory (remember: memory can only store bits). For example, if you assign an integer object the value 65, that value is converted to the sequence of bits 0100 0001 and stored in the memory assigned to the object.
Conversely, when the object is evaluated to produce a value, that sequence of bits is reconstituted back into the original value. Meaning that 0100 0001 is converted back into the value 65.
Fortunately, the compiler and CPU do all the hard work here, so you generally don’t need to worry about how values get converted into bit sequences and back.
All you need to do is pick a data type for your object that best matches your desired use.
谨记:内存只能存 bit,只能寻址寻到 byte 这一层,如果数据按内存边界对齐,寻址会更快(一次读)。
由于内存地址空间有限,且按 bit 寻址的场景很少,所以寻址单位一般是 byte。A byte is a group of bits that are operated on as a unit. The modern standard is that a byte is comprised of 8 sequential bits.
|
|
从内存连续 bit 来看,a 和 b 都是存了 4 byte 的 1,区别仅仅是 data type 不一样,导致了截然不同的结果。
移位操作
a 和 b 左移一位都得到:
0xfffffffe: 如果是 int 解释为-2, unsigned int 解释为 4294967294=2^32 - 2
a 右移一位得到
0xffffffff: 注意负数右移,高位补 1,int 解释为-1
b 右移一位得到
0x7fffffff: 高位补 0, unsigned int 解释为 2147483647=2^31-1
注意,负的可能左移成正的,因此,有符号的移位是不安全的。
|
|
NOTE:
--
运算符,可能减至负数溢出std::int8_t
和std::uint8_t
可能知识char
和unsigned char
的别名,可能有坑(参考:https://www.learncpp.com/cpp-tutorial/introduction-to-type-conversion-and-static_cast/)Best practice
Favor signed numbers over unsigned numbers for holding quantities (even quantities that should be non-negative) and mathematical operations. Avoid mixing signed and unsigned numbers.
|
|
字节序就是计算机存储数据的时候将低位数据存在低位地址还是高位地址。举个例子,数值 0x2211 使用两个字节储存:高位字节是 0x22,低位字节是 0x11。
如果太多记不住,至少要记住:
既然如此,我们要判断一台机器是 big-endian 还是 little-endian,只需要构造一端内存,按字节从低位地址向高位地址访问,看看低位地址存的是高位字节,还是低位字节即可。
且看上述代码,构造了一个整数 0x01020304,然后通过将首地址转成char*
的方式去按字节读取内存中的值(这样做的目的是,char*
可以逐字节的读取内存;而int*
一次指针移动会移动sizeof(int)
个字节)。读出来如果是符合书写习惯的 1234,则表明机器是 big-endian,反之 little-endian.
这也是一段内存的两种不同的解释方式,recall that Because all data on a computer is just a sequence of bits, we use a data type (often called a “type” for short) to tell the compiler how to interpret the contents of memory in some meaningful way.
Cf. https://www.learncpp.com/cpp-tutorial/internal-linkage/
Identifiers have another property named linkage
. An identifier’s linkage determines whether other declarations of that name refer to the same object or not.
Local variables have no linkage
, which means that each declaration refers to a unique object.
Global variable and functions identifiers can have either internal linkage
or external linkage
.
An identifier with internal linkage can be seen and used within a single file, but it is not accessible from other files (that is, it is not exposed to the linker). This means that if two files have identically named identifiers with internal linkage, those identifiers will be treated as independent.
To make a non-constant global variable internal, we use the static keyword.
|
|
To see it, we take
a.cpp:
|
|
main.cpp:
|
|
if we compile only main.cpp, it works fine and outputs:
glabal variable (g_x, g_y, g_z) is (222, 333, 444)
But if we compile both, it gets
|
|
As we sligtly modify main.cpp:
|
|
it’s compiled and linked properly with the output:
glabal variable (g_x, g_y, g_z) is (22, 333, 444)
noting that the g_x
has the value 22 which is defined in a.cpp, we find out the global non-const variable has external linkage. And the properly compilation and linking show that global const has internal linkage.
Cf. https://www.learncpp.com/cpp-tutorial/external-linkage/
An identifier with external linkage can be seen and used both from the file in which it is defined, and from other code files (via a forward declaration). In this sense, identifiers with external linkage are truly “global” in that they can be used anywhere in your program!
Functions have external linkage by default
In order to call a function defined in another file, you must place a forward declaration
for the function in any other files wishing to use the function. The forward declaration tells the compiler about the existence of the function, and the linker connects the function calls to the actual function definition.
Global variables with external linkage
Global variables with external linkage are sometimes called external variables. To make a global variable external (and thus accessible by other files), we can use the extern
keyword to do so:
|
|
Non-const global variables are external by default (if used, the extern
keyword will be ignored).
To actually use an external global variable that has been defined in another file, you also must place a forward declaration
for the global variable in any other files wishing to use the variable. For variables, creating a forward declaration is also done via the extern
keyword (with no initialization value).
Here is an example of using a variable forward declaration:
a.cpp:
|
|
main.cpp:
|
|
Note that the extern
keyword has different meanings in different contexts. In some contexts, extern
means “give this variable external linkage”. In other contexts, extern
means “this is a forward declaration for an external variable that is defined somewhere else”.
Summary
Scope determines where a variable is accessible. Duration determines where a variable is created and destroyed. Linkage determines whether the variable can be exported to another file or not.
考虑如下场景,有一段代码很独立,适合抽成一个函数,但你又担心函数调用开销,此时 inline function 就是你的最佳选择。关于合适使用 inline function,下面这段话给了一定的意见:
For functions that are large and/or perform complex tasks, the overhead of the function call is typically insignificant compared to the amount of time the function takes to run. However, for small functions, the overhead costs can be larger than the time needed to actually execute the function’s code! In cases where a small function is called often, using a function can result in a significant performance penalty over writing the same code in-place.
Inline function 的好处包括:
However, inline expansion has its own potential cost: if the body of the function being expanded takes more instructions than the function call being replaced, then each inline expansion will cause the executable to grow larger. Larger executables tend to be slower (due to not fitting as well in caches).
注意:inline 只是对编译器的一个建议,是否会真的展开取决于编译器的优化策略。
However, in modern C++, the inline
keyword is no longer used to request that a function be expanded inline. There are quite a few reasons for this:
inline
to request inline expansion is a form of premature optimization, and misuse could actually harm performance.inline
keyword is just a hint – the compiler is completely free to ignore a request to inline a function. This is likely to be the result if you try to inline a lengthy function! The compiler is also free to perform inline expansion of functions that do not use the inline
keyword as part of its normal set of optimizations.inline
keyword is defined at the wrong level of granularity. We use the inline
keyword on a function declaration, but inline expansion is actually determined per function call. It may be beneficial to expand some function calls and detrimental to expand others, and there is no syntax to affect this.注意:在 modern cpp 中,用 inline 修饰的不违反 ODR(one definition rule),因此可用于
Allowing functions with a constexpr return type to be evaluated at either compile-time or runtime was allowed so that a single function can serve both cases. Otherwise, you’d need to have separate functions (a constexpr version and a non-constexpr version) – and since return type isn’t considered in function overload resolution, you’d have to name the functions different things!
A constexpr function that is eligible to be evaluated at compile-time will only be evaluated at compile-time if the return value is used where a constant expression is required. Otherwise, compile-time evaluation is not guaranteed.
Thus, a constexpr function is better thought of as “can be used in a constant expression”, not “will be evaluated at compile-time”.
An unnamed namespace (also called an anonymous namespace) is a namespace that is defined without a name, like so:
|
|
特点:
解决的问题:Unnamed namespaces will also keep user-defined types (something we’ll discuss in a later lesson) local to the file, something for which there is no alternative equivalent mechanism to do.
About switch
clause
Put another way, defining a variable without an initializer is just telling the compiler that the variable is now in scope from that point on. This happens at compile time, and doesn’t require the definition to actually be executed at runtime.
|
|
The syntax for creating a non-const function pointer is one of the ugliest things you will ever see in C++:
// fcnPtr is a pointer to a function that takes no arguments and returns an integer
int (*fcnPtr)();
In the above snippet, fcnPtr is a pointer to a function that has no parameters and returns an integer. fcnPtr can point to any function that matches this type.
To make a const function pointer, the const goes after the asterisk:
int (*const fcnPtr)();
If you put the const before the int, then that would indicate the function being pointed to would return a const int.
废话以后有时间再加。
首先编译时开启调试选项:
|
|
-O0
指定编译器的优化级别为 0,即不优化。
最近实习接触到一个新的知识点,C/C++ 的位域结构体。
以下开始摘抄自:here
位段 (bit-field) 是以位为单位来定义结构体 (或联合体) 中的成员变量所占的空间。含有位段的结构体 (联合体) 称为位段结构。采用位段结构既能够节省空间,又方便于操作。
总结一下这几个月的面试经历中被问到的问题,虽说问得都很浅,但是,问深了我也不会呀!
Q: std::vector
push_back 的复杂度是多少?
A: O(1), amortized constant.
Q: vector 从 1 到 n push n 个元素,假设发生扩容时按两倍增长,写出复杂度关于 n 的表达式? A: 不会。
Declaration: this article is in long time editing…
Here comes some beautiful recursive solutions to some problems.
Some of the problems have a very nice recursive structure, we can deal with them just using one step recursion.
The first comes very famous Fibonacci Numbers, which is a sequence of 0, 1, 1, 2, 3, 5, 8, 13 … The structure is easily captured, if we use $\text{fib}(n)$ to denote the $n^{\text{th}}$ Fibonacci Number (n is assumed to start from 0). $$ \text{fib}(n) = \begin{cases} n, &\text{ if } n \le 1 \newline \text{fib}(n-1) + \text{fib}(n-2), &\text{ if } n > 1. \end{cases} $$ That is why we can write easily a procedure to compute the fibs. If we use MIT Scheme, we can write as follows:
In editing…
首先要对原码、反码、补码有一定理解,推荐阅读此文:https://www.cnblogs.com/zhangziqiu/archive/2011/03/30/computercode.html
一个长度为$N$的序列,玩家每次只能从头部或尾部拿数字,不能从中间拿。拿走的数字依次从左到右排列在自己面前。拿完$N$个数字之后,游戏结束。此时$N$个数字在玩家面前组成一个新的排列,这个数列每相邻两个数字之差的绝对值之和为玩家最终得分。假设玩家前面的$N$个数字从左到右标号为 $n_1,n_2, \dots, n_N$,则最终得分$S$的计算方式如下: $$ S = \text{abs}(n_1-n_2) + \text{abs}(n_2-n_3) + \cdots + \text{abs}(n_{N-1} - n_N). $$