Reading Note of CppPrimer-Chapter2
Variables & Basic Types
基本内置类型 (basic built-in types)
unsigned 和 signed 在一起算术运算,在某种情况下结果会自动转换为 unsigned [参见 chapter4.14: 无符号类型的隐形转换],如果运算结果小于 0,则得到一个很大的正整数;unsigned 的数据类型,越界的时候会自动取模,但是 signed 的数据类型,数值越界未定义;
1
2
3
4unsigned u = 10;
int i = -42;
std::cout << i + i << std::endl; // get -84
std::cout << i + u << std::endl; // if int is 32bit, get 4294967264char 在不同平台的实现不一样,有可能是 unsigned 也有可能是 signed, 所以最好不要将 char 用于算数运算,非要用的时候要明确 signed 或者 unsigned;
The signedness of char depends on the compiler and the target platform: the defaults for ARM and PowerPC are typically unsigned, the defaults for x86 and x64 are typically signed.
在代码里面使用 float 常量的时候 (例如 std::max (a, 1.0)),避免出现 1.0 被解析成 double 但是 a 却是 float,可以将 1.0 写成 1.0f or 1.0F;
1
2
3float var = 1.0;
std::max(var, 2.0); // invalid, compare float with double
std::max(var, 2.0f); // valid, both float字面值常量 (literal)
整形和浮点型常量,包含 2、8、10、16 进制
1
2
3
4二进制 0b+ (0-1) 0b1010101
八进制 o+ (0-7) o125
十进制 --- (0-9) 85
十六进制 0x+ (0-9,A-F) 0x55 #前缀可大写字符和字符串常量
单引号括起来的是字符,如
'A'
;双引号括起来的是字符串字面常量 (string literal),如"A"
;字符串字面常量实际上是由常量字符构成的数组,编译器会在每个字符串的结尾处添加 \0;因此字符串字面常量的实际长度要比他的内容多 1
1
std::cout << sizeof("ABC") << std::endl; // print 4
很长的字符串常量如果一行写不下的时候,可以换行书写,使得字符串的两部分都被双引号括起来
1
2std::cout << "A very very very very very long string literal "
"that span two lines" << std::endl;
转义序列
1
'\n'; '\r'; '\t'
布尔字面值和指针字面值
1
true; false; nullptr;
可以通过前缀后缀来制定字面值的类型:
suffix:
L
,LL
,U
,F
,LF
,ULL
,UL
prefix:
u8
;
变量初始化 (variables initialization)
初始化不是赋值, 赋值涉及擦除原有数据;
比如对于一个类来说,初始化仅仅执行构造函数,但是赋值会调构造函数和析构函数;
记住初始化任何一个定义的变量,不管用不用,可以用最简单的 0,nullptr 之类的初始化;
内置类型的初始化变量尽量用
{}
来实现,有一个数据丢失的检查,可以防止无意的 bug;1
2
3// 涉及到数据类型从大到小的转换的时候,编译器会报错
bool is_float = 3.4f;
short short_num = 10000;1
2bool is_float_{3.4f}; // compile error
char short_num_{10000}; // compile error1
2
3
4error: type 'float' cannot be narrowed to 'bool' in initializer list [-Wc++11-narrowing]
bool is_float_{3.4f};
error: constant expression evaluates to 10000 which cannot be narrowed to type 'char' [-Wc++11-narrowing]
char short_num_{10000};尽量使用 nullptr 初始化一个指针,而不是 NULL;在 C++11 里面,NULL 和 nullptr 不完全一样;NULL 会有重载的歧义(既可以解读为
void*
,也是int 0
1
2
3
4
5
6
7
8
9
10// NULL is a “manifest constant” (a #define of C) that’s actually an integer that can be assigned to a pointer because of an implicit conversion.
// nullptr is a keyword representing a value of self-defined type, that can convert into a pointer, but not into integers.
int i = NULL; // ok
int i = nullptr; // error - not a integer convertible value
int* p = NULL; // ok - int converted into pointer
int* p = nullptr; // ok
// nullptr is always a pointer type. 0 (aka. C's NULL bridged over into C++) could cause ambiguity in overloaded function resolution, among other things:
f(int); // NULL will call this one, not call f(foo *) because of a int-to-pointer conversion is needed
// while f(int) is perfectly matched
f(foo *); // nullptr will call the onestd::nullptr_t
is the type of the null pointer literal,nullptr
is the type of the null pointer literal,nullptr
;sizeof(std::nullptr_t)
is equal tosizeof(void *)
;1
2sizeof(nullptr) == sizeof(void *); // true
sizeof(void *) == 8; // true默认初始化
默认初始化是定义对象时,没有使用初始化器 (initializer),也即没有做任何初始化说明时的默认行为;
有三种情况会执行默认初始化:
有 automatic、static、thread_local 存储期 (storage duration) 的变量如果没有给定初始化器,是被默认初始化的;比如一个块作用域中的局部变量 (内置变量、复合类型变量),但是默认初始化全局变量,静态变量、thread_local 变量的时候,会首先被零初始化,所以值是合法的;
使用
new
操作符创建一个对象的时候如果没有指定初始化器,也是被默认初始化;基类或者类的非静态成员没有出现在类的构造函数的初始化列表也是被默认初始化;
默认初始化的方式取决于对象的类型:
- 如果默认初始化的对象是类类型,则被调用的默认构造函数为类的成员提供初始值;如果类成员没有初始值,则值未定义
- 如果默认初始化的对象是数组,则对数组的每个成员都执行默认初始化;
- 其他情况,就不做任何初始化,所有拥有 automatic 存储期的对象的值及其包含的对象都是不确定的 (indeterminate),使用和操作也会带来不确定 的结果;
1
2
3
4
5
6
7
8
9
10
11
12
13
14struct T2 {
int mem;
T2() { } // "mem" is not in the initializer list, default init
};
// block scope, automatic storage duration
{
int n; // non-class, the value is indeterminate
int a[2]; // array, each element of the array is default initialized and is indeterminate
std::string s; // class, calls default ctor, the value is "" (empty string)
std::string a[2]; // array, default-initializes the elements, the value is {"", ""}
NoInitializer *objptr = new NoInitializer; // objptr->mem == 0, for objptr has dynamic storage duration
NoInitializer obj;// obj.mem is indeterminate, for obj has automatic storage duration
}有时候
new
一个对象的时候没有指定初始化器,即使默认初始化得到的是 0,但这也只是巧合,并不能得到保证;所以new
的时候记得加上空的括号()
或者花括号{}
使得new
调用值初始化1
2
3
4
5
6
7
8
9// c maybe be always 0
auto c = new int;
// however
int* te = new int[4]{1,2,3,4}; // te: 1 2 3 4
delete [] te;
te = new int[4]; // te: 0 0 2043 4 (test on macos and apple clang 13.1.6)
delete [] te;
te = new int[4](); // te: 0 0 0 0For most systems, this(init to zero) is only true when the memory came directly from the OS (for security reasons). When it came from memory cached by the allocator, it most likely contains whatever it did before it was freed. This is also a reason why such bugs sometimes do not arise in testcases/unit tests, but only after a while of the “real” program running. Valgrind to the rescue here. stackoverflow
值初始化
值初始化是定义对象时,要求初始化,但没有给出初始值的行为,一般给空的
()
{}
作为 initializer-list;有四种情况会执行零初始化
- 当一个对象在创建时指定了空的
()
或者{}
作为初始化器, 如std::vector<int>()
- 当在
new
一个对象时指定了空的()
或者{}
作为初始化器, 如new T{}
- 当一个类的非静态成员或者基类在初始化列表里面通过空的
()
或者{}
作为初始化器 - 有 automatic、static、thread_local 存储期 (storage duration) 的变量使用空的
()
或者{}
作为初始化器
值初始化的行为根据初始化的对象的不同而不同:
- 如果是类类型且因为各种原因没有默认构造函数 (比如定义了别的构造函数或者默认构造函数设为 deleted),则类执行默认初始化
- 如果是类类型且类有默认构造函数而且这个默认构造函数不是用户定义的,则类执行零初始化
- 如果对象是数组,则对数组的每个成员执行值初始化
- 其他情况执行零初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22struct T1 {
int mem1;
std::string mem2;
}; // implicit default constructor
struct T3 {
int mem1;
std::string mem2;
T3() {} // user-provided default constructor
};
{
int n{}; // scalar => zero-initialization, the value is 0
double f = double(); // scalar => zero-initialization, the value is 0.0
int* a = new int[10](); // array => value-initialization of each element
T1 t1{}; // class with implicit default constructor =>
// t1.mem1 is zero-initialized, the value is 0
// t1.mem2 is default-initialized, the value is ""
T3 t3{}; // class with user-provided default constructor =>
// t3.mem1 is default-initialized to indeterminate value
// t3.mem2 is default-initialized, the value is ""
}- 当一个对象在创建时指定了空的
零初始化
有三种情况会使用零初始化:
- 对于 static、thread_local 存储期 (storage duration) 的变量 (static duration includes static and global),零初始化将会在其他类型的初始化之前先初始化
- 在值初始化某些非类类型的对象以及类类型对象的成员时会执行零初始化;
- 当使用字符串字面值初始化字符数组的时候,数组中没有被填满剩余的元素字符会被零初始化
零初始化的行为根据初始化的对象的不同而不同:
- 如果对象的类型是数量类型 (scalar type),则会被赋值 0,比如 int、float、指针、枚举类型
- 如果对象的类型是非 union 类,基类和数据成员会零初始化,所有的 padding 设为 0;类的构造函数会被忽略 (一般是合成默认构造函数);
- 如果对象的类型是 union 类,则第一个非静态数据成员被零初始化,所有的 padding 设为 0;
- 如果对象的类型是数组,则数组的每一个成员都执行零初始化
1
2
3
4
5
6
7
8
9
10
11
12
13static int n; // static non-class, a two-phase initialization is done:
// 1) zero initialization initializes n to zero
// 2) default initialization does nothing, leaving n being zero
// global variables have static storage duration
double f[3]; // zero-initialized to three 0.0's
int* p; // zero-initialized to null pointer value
// (even if the value is not integral 0)
int main(int argc, char*[])
{
delete p; // safe to delete a null pointer
static int n = argc; // zero-initialized to 0 then copy-initialized to argc
}直接初始化
直接初始化是指通过显式的制定构造参数来初始化一个对象,在以下情况使用直接初始化:
- 使用非空的包含若干参数的括号
()
来初始化对象,比如T t(arg)
,T t(arg1, arg2)
; - 使用花括号来初始化非类类型的对象,比如
T t{arg}
; - 使用
function_cast
(比如强制类型转换 int (1.2)) 或者static_cast
来初始化一个纯右值; - 使用
new
创建对象的时候提供的非空的初始化器; - 通过类的构造函数的初始化列表来初始化类的非静态成员或者基类;
- 初始化 lambda 表达式中通过
拷贝捕获(by-copy capture)
的对象;
直接初始化的行为根据初始化的对象的不同而不同:
- 如果初始化的对象是类类型,则会在重载的所有解决方案 (e.g. ctor) 中找到一个最好的匹配,然后使用构造函数初始化对象;
- 如果初始化对象不是类类型但是初始值是类类型,则会在这个类及其基类中找到最适合的转换函数去初始化该对象
- 如果初始化对象是 bool,
std::nullptr_t
会将搞对象初始化为false
; - 其他情况会调用类型转换,将初始值转化为对象的类型
1
2
3
4
5
6
7
8
9
10
11std::string s1("test"); // constructor from const char*
std::string s2(10, 'a');
int a{10};
int *iptr = new int(5);
auto lambda_func = [a](){std::cout << a << std::endl;}; // a: copy capture
class T{
int mem = 0; // init value will be ignored for mem is directly initialized in member init list
T():mem(1) {} // direct initialization of mem
};- 使用非空的包含若干参数的括号
拷贝初始化
拷贝初始化是指从另一个对象初始化对象,一下情况会使用拷贝初始化:
- 当使用等号
=
把右边表达式赋值给一个左边的非引用对象时 - 函数传值调用的时候 (passing an argument by value)
- 从函数返回一个对象的时候 (function that returns by value)
- 当通过值来 throw 或者 catch 异常的时候
- 数组初始化的时候,对于初始化器提供了的值,执行拷贝初始化,剩余的对象执行零初始化;
拷贝初始化的行为根据初始化的对象的不同而不同:
- 对于类类型,只能在非显式 (no-explicit) 的构造函数中匹配最合适的来初始化该对象
- 否则执行标准的类型转换,将初始值转换为对象的类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int n = 3.14; // floating-integral conversion
const int b = n; // const doesn't matter
std::string s = "test"; // OK: constructor is non-explicit
std::string s2 = std::move(s); // this copy-initialization performs a move
struct A {
operator int() { return 12;}
};
struct B {
B(int) {}
};
A a;
B b0 = 12; // implicit ctor B(int)
B b2{a}; // calling A::operator int(), then B::B(int)
B b3 = {a}; // identical- 当使用等号
列表初始化
列表初始化是 C++11 新给出的一种初始化方式,可用于内置类型,也可以用于自定义对象,一般用
{}
来;列表初始化一般分为直接列表初始化和拷贝列表初始化,分别对应直接初始化和拷贝初始化
1
2T object { arg1, arg2, ... }; //直接列表初始化
T object = {arg1, arg2, ...}; //拷贝列表初始化1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28int n0{}; // value-initialization (to zero)
int n1{1}; // direct-list-initialization
std::string s1{'a', 'b', 'c', 'd'}; // initializer-list constructor call
std::string s2{s1, 2, 2}; // regular constructor call
std::string s3{0x61, 'a'}; // initializer-list ctor is preferred to (int, char)
int n2 = {1}; // copy-list-initialization
double d = double{1.2}; // list-initialization of a prvalue, then copy-init
auto s4 = std::string{"HelloWorld"}; // same as above, no temporary created since C++17
std::map<int, std::string> m = // nested list-initialization
{
{1, "a"},
{2, {'a', 'b', 'c'}},
{3, s1}
};
std::pair<std::string, std::string> f(std::pair<std::string, std::string> p) {
return {p.second, p.first}; // list-initialization in return statement
}
f({"hello", "world"}).first; // list-initialization in function call
struct Foo {
std::vector<int> mem = {1, 2, 3}; // list-initialization of a non-static member
std::vector<int> mem2;
Foo() : mem2{-1, -2, -3} {} // list-initialization of a member in constructor
};
变量声明和定义 (Variable Declaration & Definition)
区分 C++ 变量的定义 (definition) 和声明 (declaration)
From the C++ standard section 3.1:
A declaration introduces names into a translation unit or redeclares names introduced by previous declarations. A declaration specifies the interpretation and attributes of these names.
The next paragraph states (emphasis mine) that a declaration is a definition unless…
… it declares a
function
without specifying the function’s body:1
void sqrt(double); // declares sqrt but no body
… it declares a
static
member within a class definition:1
2
3
4
5
6static int c{0}; // definitdefines c as a static var outside a class definition
struct X
{
int a; // defines a
static int b; // declares b as a static var within a class definition
};… it declares a
class
name:1
class Y; // declare a class
… it contains the
extern
keyword without an initializer or function body:1
2
3
4
5
6extern const int i = 0; // defines i as extern but with init, so it is a defination
extern int j; // declares j as extern but without init
extern "C"
{
void foo(); // declares foo
}… or is a
typedef
orusing
statement.1
2typedef long LONG_32; // declares LONG_32
using namespace std; // declares std
类静态变量只是声明,没有定义,否则会出现
undefined reference
(before c++17),即 static data members (const or not) 有 external linkage,参见本章 external linkage1
2
3
4
5
6
7
8
9
10
11
12
13
14class foo
{
public:
static int bar; // declaration of static data member
// Error: ISO C++ forbids in-class initialization of non-const static member
static int bar {10};
// Valid
static const int bar {10};
};
// We have to explicitly define the static variable, otherwise it will result in a
// undefined reference to 'foo::bar'
int foo::bar = 0; // actural definition of data memberStarting from C++17 you can declare your static members as
inline
. This eliminates the need for a separate definition. By declaring them in that fashion you effectively tell compiler that you don’t care where this member is physically defined and, consequently, don’t care about its initialization order.1
2
3
4
5
6
7
8class foo
{
public:
inline static int bar; // declaration of static data member
};
// Error: if bar declare as inline, the line will result in redefinition error
int foo::bar = 0;理解 c++ 语言中为啥声明和定义需要区分
From the beginning of time C++ language, just like C, was built on the principle of independent translation. Each translation unit is compiled by the compiler proper independently, without any knowledge of other translation units. The whole program only comes together later, at linking stage. Linking stage is the earliest stage at which the entire program is seen by linker (it is seen as collection of object files prepared by the compiler proper).
In order to support this principle of independent translation, each entity with external linkage has to be defined in one translation unit, and in only one translation unit. The user is responsible for distributing such entities between different translation units. It is considered a part of user intent, i.e. the user is supposed to decide which translation unit (and object file) will contain each definition.
The same applies to static members of the class.Static data members of the class are entities with external linkage. The compiler expects you to define that entity in some translation unit. The whole purpose of this feature is to give you the opportunity to choose that translation unit. The compiler cannot choose it for you. It is, again, a part of your intent, something you have to tell the compiler.
This is no longer as critical as it used to be a while ago, since the language is now designed to deal with (and eliminate) large amount of identical definitions (templates, inline functions, etc.), but the One Definition Rule is still rooted in the principle of independent translation.
In addition to the above, in C++ language the point at which you define your variable will determine the order of its initialization with regard to other variables defined in the same translation unit. This is also a part of user intent, i.e. something the compiler cannot decide without your help.
reference missing
申明和定义全局变量可能犯的错
Possibility1
Perhaps you forgot to declare the variable in the other translation unit (TU). Here’s an example:
1
2
3
4
5
6
7
8
9
10
11//// a.cpp
int x = 5; // declaration and definition of my global variable
//// b.cpp
// I want to use `x` here, too.
// But I need b.cpp to know that it exists, first:
extern int x; // declaration (not definition)
void foo() {
cout << x; // OK
}Possibility2
Additionally, it’s possible that the variable has internal linkage, meaning that it’s not exposed across translation units. This will be the case by default if the variable is marked
const
([C++11: 3.5/3]
)1
2
3
4
5
6
7
8
9//// a.cpp
const int x = 5; // file-`static` by default
//// b.cpp
extern const int x; // says there's a `x` that we can use somewhere...
void foo() {
cout << x; // ... but actually there isn't. So, linker error.
}You could fix this by applying
extern
to the definition, too:1
extern const int x = 5; // now with extern linkage
变量存储限定符 (Variable Storage Class Specifier)
Storage class specifiers:
The storage class specifiers are a part of the decl-specifier-seq of a name’s declaration syntax. Together with the scope of the name, they control two independent properties of the name: its storage duration and its linkage.
The static storage class is used to declare an identifier that is a local variable either to a function or a file and that exists and retains its value after control passes from where it was declared. This storage class has a duration that is permanent. A variable declared of this class retains its value from one call of the function to the next. The scope is local. A variable is known only by the function it is declared within or if declared globally in a file, it is known or seen only by the functions within that file. This storage class guarantees that declaration of the variable also initializes the variable to zero or all bits off.
The storage for the object is allocated when the program begins and deallocated when the program ends. Only one instance of the object exists. All objects declared at namespace scope (including global namespace) have this storage duration, plus those declared with
static
orextern
.Variables declared at block scope with the specifier
static
orthread_local
(since C++11) have static or thread (since C++11) storage duration but are initialized the first time control passes through their declaration (unless their initialization is zero- or constant-initialization, which can be performed before the block is first entered).The
static
specifier is only allowed in the declarations of objects (except in function parameter lists), declarations of functions (except at block scope), and declarations of anonymous unions. When used in a declaration of a class member, it declares a static member. **When used in a declaration of an object, it specifies static storage duration (except if accompanied bythread_local
). When used in a declaration at namespace scope, it specifies internal linkage.The extern storage class is used to declare a global variable that will be known to the functions in a file and capable of being known to all functions in a program. This storage class has a duration that is permanent. Any variable of this class retains its value until changed by another assignment. The scope is global. A variable can be known or seen by all functions within a program.
The
extern
specifier is only allowed in the declarations of variables and functions (except class members or function parameters).It specifies external linkage, and does not technically affect storage duration, but it cannot be used in a definition of an automatic storage duration object, so all
extern
objects have static or thread durations. In addition, a variable declaration that usesextern
and has no initializer is not a definition.The thread_local keyword is only allowed for objects declared at namespace scope, objects declared at block scope, and static data members.
It indicates that the object has thread storage duration. It can be combined with
static
orextern
to specify internal or external linkage (except for static data members which always have external linkage), respectively, but that additionalstatic
doesn’t affect the storage duration.The storage for the object is allocated when the thread begins and deallocated when the thread ends. Each thread has its own instance of the object. Only objects declared
thread_local
have this storage duration. Note that main thread is also a thread, so the main thread starts, the global thread_local object is allocated.这里有一个很重要的信息,就是
static thread_local
和thread_local
声明是等价的,都是指定变量的周期是在线程内部,并且是静态的。下面是一个线程安全的均匀分布随机数生成,例子来源于 stackoverflow:1
2
3
4
5
6
7
8inline void random_uniform_float(float *const dst, const int len, const int min=0, const int max=1) {
// generator is only created once in per thread, but distribution can be regenerated.
static thread_local std::default_random_engine generator; // heavy
std::uniform_real_distribution<float> distribution(min, max); // light
for (int i = 0; i < len; ++i) {
dst[i] = distribution(generator);
}
}generator
是一个函数的静态变量,理论上这个静态变量在函数的所有调用期间都是同一个的(静态存储期),相反distribution
是每次调用生成的函数内临时变量。现在generator
被 thread_local 修饰,表示其存储周期从整个函数调用变为了线程存储期,也就是在同一个线程内,这个变量表现的就和函数静态变量一样,但是不同线程中是不同的。可以理解为 thread_local 缩小了变量的存储周期。关于 thread_local 变量自动 static,C++ 标准中也有说明:When thread_local is applied to a variable of block scope the storage-class-specifier static is implied if it does not appear explicitly
thread_local code example 来源 murphype’s Blog
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30thread_local int x = 1;
void thread_func(const std::string& thread_name) {
for (int i = 0; i < 3; ++i) {
thread_local int y = 10;
x++;
y++;
std::lock_guard<std::mutex> lock(cout_mutex);
std::cout << "thread[" << thread_name << "]: x = " << x << " , y = " << y << std::endl;
}
return;
}
int main() {
std::thread t1(thread_func, "t1");
std::thread t2(thread_func, "t2");
t1.join();
t2.join();
std::cout << "after thread, thread local x: " << x << std::endl;
return 0;
}
// output
thread[t1]: x = 2 , y = 11
thread[t1]: x = 3 , y = 12
thread[t1]: x = 4 , y = 13
thread[t2]: x = 2 , y = 11
thread[t2]: x = 3 , y = 12
thread[t2]: x = 4 , y = 13
after thread, thread local x: 1Reference: What_is_the_difference_between_static_and_extern cppreference-storage_duration cppreference-scope
变量链接 (Variable Linkage)
-
A name that denotes object, reference, function, type, template, namespace, or value, may have linkage.
If a name has linkage, it refers to the same entity as the same name introduced by a declaration in another scope. If a variable, function, or another entity with the same name is declared in several scopes, but does not have sufficient linkage, then several instances of the entity are generated.
The following linkages are recognized:
no linkage: The name can be referred to only from the scope it is in.
Any of the following names declared at block scope have no linkage:
- variables that aren’t explicitly declared
extern
(regardless of thestatic
modifier); - local classes and their member functions;
- other names declared at block scope such as typedefs, enumerations, and enumerators.
Names not specified with external, module, (since C++20) or internal linkage also have no linkage, regardless of which scope they are declared in.
- variables that aren’t explicitly declared
internal linkage: the name can be referred to from all scopes in the current translation unit.
Any of the following names declared at namespace scope have internal linkage:
- variables, variable templates (since C++14), functions, or function templates declared
static
; - non-volatile non-template (since C++14) non-inline (since C++17) non-exported (since C++20) const-qualified variables (including constexpr) that aren’t declared
extern
and aren’t previously declared to have external linkage; - data members of anonymous unions.
- In addition, all names declared in unnamed namespace or a namespace within an unnamed namespace, even ones explicitly declared
extern
, have internal linkage.
Global variables with internal linkage are sometimes called internal variables.
1
2
3
4
5
6
7
8
9
10
11
12
13// Internal global variables definitions:
// non-constant globals have external linkage by default,
// but can be given internal linkage via the static keyword
static int g_x; // defines non-initialized internal global variable (zero initialized by default)
static int g_x{ 1 }; // defines initialized internal global variable
// const globals have internal linkage by default
const int g_y { 2 }; // defines initialized internal global const variable
constexpr int g_y { 3 }; // defines initialized internal global constexpr variable
// Internal function definitions:
static int foo() {}; // defines internal function- variables, variable templates (since C++14), functions, or function templates declared
external linkage: the name can be referred to from the scopes in the other translation units. Variables and functions with external linkage also have language linkage, which makes it possible to link translation units written in different programming languages.
Any of the following names declared at namespace scope have external linkage, unless they are declared in an unnamed namespace or their declarations are attached to a named module and are not exported (since C++20):
- variables and functions not listed above (that is, functions not declared
static
, non-const variables not declaredstatic
, and any variables declaredextern
); - enumerations;
- names of classes, their member functions, static data members (const or not), nested classes and enumerations, and functions first introduced with friend declarations inside class bodies;
- names of all templates not listed above (that is, not function templates declared
static
).
Any of the following names first declared at block scope have external linkage:
- names of variables declared
extern
; - names of functions.
- variables and functions not listed above (that is, functions not declared
引用类型 (Reference)
Introduction to reference
左值引用 (lvalue references)
Lvalue references can be used to alias an existing object (optionally with different cv-qualification)
右值引用 (Rvalue references)
Rvalue references can be used to extend the lifetimes of temporary objects (note, lvalue references to const can extend the lifetimes of temporary objects too, but they are not modifiable through them):
1
2
3
4
5
6
7
8
9std::string s1 = "Test";
// std::string&& r1 = s1; // error: can't bind to lvalue
const std::string& r2 = s1 + s1; // okay: lvalue reference to const extends lifetime
// r2 += "Test"; // error: can't modify through reference to const
std::string&& r3 = s1 + s1; // okay: rvalue reference extends lifetime
r3 += "Test"; // okay: can modify through reference to non-const
std::cout << r3 << '\n';转发引用 (Forwarding references)
forwarding references are a special kind of references that preserve the value category of a function argument, making it possible to forward it by means of std::forward. Forwarding references are either:
function parameter of a function template declared as rvalue reference to cv-unqualified type template parameter of that same function template:
1
2
3
4
5
6
7
8template<class T>
int f(T&& x) { // x is a forwarding reference
return g(std::forward<T>(x)); // and so can be forwarded
}
int i;
f(i); // argument is lvalue, calls f<int&>(int&), std::forward<int&>(x) is lvalue
f(0); // argument is rvalue, calls f<int>(int&&), std::forward<int>(x) is rvalueauto&&
except when deduced from a brace-enclosed initializer list:1
2
3
4for (auto&& x: f()) {
// x is a forwarding reference; this is the safest way to use range for loops
}
auto&& z = {1, 2, 3}; // NOT a forwarding reference (special case for initializer lists)
References are not objects; they do not necessarily occupy storage, although the compiler may allocate storage if it is necessary to implement the desired semantics (e.g. a non-static data member of reference type usually increases the size of the class by the amount necessary to store a memory address).
Because references are not objects, there are no arrays of references, no pointers to references, and no references to references(except reference collapse)
1
2
3int& a[3]; // error
int&* p; // error
int& &r; // errorbut we have reference to pointer:
1
2int * p =0;
int* &q = p; // q就是指向指针的引用
引用必须在定义的时候初始化,而且引用一经绑定,无法再和其他对象绑定;c++ 语法上也不支持切换引用的对象,如
auto &b = aa
; 引用初始化必须是对象,不能是字面值 (literal value);但是右值引用 (rvalue-reference) 和常量左值引用 (const lvalue reference) 除外1
2
3
4//std::string& b = "1"; // invalid
const int& alr = 1; // valid, but alf can NOT be modified later
int&& arf = 1; // valid, arf can be modified引用的类型必须与其所引用的对象的类型一致,有两个意外:
- 初始化 const lvalue reference 的时候允许用任意表达式,包阔字面值、变量、常量等,甚至是临时变量
1
2
3
4int i = 42;
const int &r1 = i; // variable
const int &r2 = 42; // literal
const int &r3 = r1 * r2; // expression- const 引用的初始化可以是非 const 的,const 仅仅限定不能通过 const 的引用本身去更改引用的对象;但是非 const 的引用不能是 const 的
1
2
3
4
5int a = 10;
const int& b = a; // valid
const int c = 1;
//int& d = c; // invalid可以将一个栈上的临时变量绑定给常量左值引用 (const lvalue reference),但是不可以绑定给非常量左值引用 (non const lvalue reference);这将延长这个临时变量的生命到这个 const lvalue reference 的生命结束;这也是类的拷贝构造函数的参数必须是 const lvalue reference 的原因之一,参见 chapter13.6
This is a C++ feature… the code is valid and does exactly what it appears to do.
Normally, a temporary object lasts only until the end of the full expression in which it appears. However, C++ deliberately specifies that binding a temporary object to a reference to const on the stack lengthens the lifetime of the temporary to the lifetime of the reference itself, and thus avoids what would otherwise be a common dangling-reference error. In the example above, the temporary returned by f() lives until the closing curly brace. (Note this only applies to stack-based references. It doesn’t work for references that are members of objects.)
1
2
3
4
5std::string f() { return "abc"; }
void g() {
const std::string& s = f(); // temporary object
std::cout << s << std::endl; // can we still use the "temporary" object?, Yes
}
常量 (Const)
不能把 const 变量的值赋值给非 const 的引用,也不能更改 const 变量,但是可以将 const 的值赋值给非 const 变量;
1
2
3const int a = 0;
int &b = a; // invalid
int c = a; // validconst int * ptr
vsint * const ptr
1
2
3
4
5
6
7
8
9const int* ptr; // pointer to const int, the underly value pointer point to is const
int* const ptr; // const pointer to int, the pointer is const,
// the the underly value pointer point to is not const
int ai = 1;
int* const cptr = &ai;
const int cai = 1;
const int* ptrc = &cai;
//int* const ptrc = &cai; // invalid, cannot initialize a variable of type 'int *const' with an rvalue of type 'const int *'const 对象 (const value) 的初始化:
- 可以是字面值(编译时初始化)
- 也可以是表达式(运行时初始化)
- const 初始化可以赋值一个最后结果可以转换为引用的表达式
- 也可以为一个非 const 变量。非 const 赋值给 const,仅仅是限制了引用的操作,对被引用的不影响;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15const char *str = "PangWong"; // 编译时初始化
const int i = get_size(); // 运行时初始化
int nci = 10;
const int& ncir = nci; // 非const变量
int a = 12;
int& add(int v){
a+=v;
return a;
}
// expression return an reference
int int& b = add(12);
// &a = 0x557a13df8010
// &b = 0x557a13df8010顶层 const (top-level const) vs 底层 const (low-level const)
顶层 const 表示指针或者引用本身是 const; 底层 const 表示指向的对象是 const
指向引用的 const 都是底层 const
指向指针的 const,在
*
后的是顶层 const,前的是底层 const指向非复合类型的 const 全是顶层 const
执行拷贝操作的时候,底层 const 和顶层 const 区别明显
顶层 const 几乎没啥影响,但是对于底层 const 而言,拷入拷出必须有相同的底层 const,或者类型可以相互转换(no-const -> const)
1
2
3
4
5
6
7
8
9
10// case 1
int b = 10;
const int &a = b; // low-level const
// case 2
const int* c = &b; // low-level const
int* const c = &b; // top-level const
// case3
const std::string d = ""; // top-level constconst vs constexpr:
对于修饰 Object 来说,const 并未区分出编译期常量和运行期常量,而 constexpr 限定在了编译期常量。
但是当 constexpr 中有编译时无法确定的值的时候,constexpr 自动退化成 runtime const;
1
2
3
4
5
6
7
8
9const double PI1 = 3.141592653589793;
constexpr double PI2 = 3.141592653589793; // guarantee to be compile-time constant
constexpr double PI3 = PI1; // error
constexpr double PI3 = PI2; // ok
// static_assert detect compile-time error
static_assert(PI1 == 3.141592653589793, ""); // error
static_assert(PI2 == 3.141592653589793, ""); // okBoth
PI1
andPI2
are constant, meaning you can not modify them. However onlyPI2
is a compile-time constant. It shall be initialized at compile time.PI1
may be initialized at compile time or run time. Furthermore, onlyPI2
can be used in a context that requires a compile-time constant.一个 constexpr 指针的初始值必须是 nullptr、0 或者存储与某个固定地址的对象,比如全局变量、静态变量等;
判断是不是 compile time const 的办法可以用
std::array<int, constexpt>
来测试或者数组的维度。通常能写成 constexpr 的尽量写成 constexpr,可以兼容 compile time and runtime;
枚举类型 (Enumerations)
enum 是一个单独的限定范围的值组成的类型,成员的默认类型是 int,也可以自定义类型;
关键字
定义一个 enum 可以使用三个
enum
、enum class
(since c++11)、enum struct
(since c++11)enum
: 没有作用域枚举;每个枚举项都成为该枚举类型(即 名字)的一个具名常量,在它的外围作用域可见,且可以用于要求常量 (constexpr) 的任何位置。enum class/struct
:有作用域枚举;它被该枚举的作用域所包含,且可用作用域解析运算符访问构造
enum name(optional) : type(optional) { enumerator = constexpr , enumerator = constexpr , … }
每个枚举项都与一个底层类型的值相关联
- 当在 枚举项列表 中提供了初始化器时,各枚举项的值由那些初始化器所定义
- 如果首个枚举项无初始化器,那么它的关联值为零
- 对于其他任何定义中无初始化器的枚举项,它的关联值是前一枚举项加一
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19enum Foo { a, b, c = 10, d, e = 1, f, g = f + c };
//a = 0, b = 1, c = 10, d = 11, e = 1, f = 2, g = 12
enum { a, b, c = 0, d = a + 2 };
// a = 0, b = 1, c = 0, d = 2
enum{d, e, f = e + 2};
// d = 0,e = 1,f = 3
// 类型默认为int,定义中无初始化器的枚举项,它的关联值是前一枚举项加一
enum color { red, yellow, green = 20, blue };
color col = red;
int n = blue; // n == 21
// altitude类型为char,可为 altitude::high 或 altitude::low
enum class altitude : char {
high='h',
low='l', // CWG518 允许额外的逗号
};using-enum
using enum
声明引入它所指名的枚举的枚举项名字,如同用对每个枚举项的 using 声明1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20struct X{
enum direction { left = 'l', right = 'r' };
};
struct Y{
using enum X::direction; // using enum inside class
};
enum fruit { orange, apple };
struct S {
using enum fruit; // OK :引入 orange 与 apple 到 S 中
};
void f() {
S s;
std::cout << static_cast<int>(s.orange) << std::endl; // OK :指名 fruit::orange
std::cout << static_cast<int>(S::orange) << std::endl; // OK :指名 fruit::orange
int b1 = Y::left;
std::cout << "Y b1: " << b1 << std::endl;
}有作用域枚举 (scope enum) vs 无作用域枚举 (unscope enum):
作用域
没有作用域的枚举在它的外围作用域可见
有作用域枚举被该枚举的作用域所包含
隐式转换
无作用域枚举类型的值可隐式转换到整型类型,整数、浮点和枚举类型的值可用
static_cast
或显式转换到任何枚举类型;1
2
3
4
5
6
7
8
9
10
11
12struct X{
enum direction { left = 'l', right = 'r' };
enum class color { red= 'r', blue= 'b' };
};
X x;
X* p = &x;
int a = X::direction::left; // allowed only in C++11 and later
int b = X::left;
int c = x.left; // 当无作用域枚举是类成员时,它的枚举项可以通过类成员访问运算符 . 和 -> 访问:
int d = p->left;
int e = static_cast<int>(X::color::red);有作用域枚举项不能隐式转换到整数类型,尽管
static_cast
可以用来获得枚举项的数值。1
2
3
4
5enum class TEnum : int {ll=1, rr=2, all=0};
std::cout << static_cast<int>(TEnum::rr) << std::endl;
// invalid operands to binary expression ('std::ostream' (aka 'basic_ostream<char>') and 'TEnum')
//std::cout << TEnum::rr << std::endl;
名字缺失
无作用域枚举的名字可以忽略,这种声明只是将各枚举项引入到它的外围作用域中
有作用域枚举的名字不可以缺失
1
2enum class : int {ll=1, rr=2, all=0};
// error: scoped enumeration requires a name
复合类型和别名 (Compound Type and Alias)
void*
看到内存仅仅是内存地址,但是对于地址里面的类型不清楚。所以void*
可以像其他指针一样作为参数、赋值、比值,但是不可以对其指向的对象操作;因为void*
事实上丢失了类型信息;有 2 种方式定义类型别名:
1
2
3
4// typedef
typedef std::vector Vector;
// using
using Vector = std::vector;指针和引用是复合类型;同时定义多个对象的时候,复合类型 qualifier 只修饰与其绑定的变量;
1
2
3int *p = 0, q = 1; // p is pointer while q is int
// equal to
int *p = 0; int q = 1;如果类型别名指代的是复合类型 (compound type),此时认为复合类型是一般类型,不可以把类型别名替换为它本来的样子以理解该语句的含义。比如:
1
2
3
4typedef char *pstring; // pstring can NOT be recognized as compound type
const pstring cstr = 0; // 这里const是修饰pstring本身,所以这里的指针是const pointer,即指向char的常量指针,
// 不等价于
const char *cstr = 0; // 这样理解出来的是指向常量的char指针