C++ Primer: Basics
本篇博客主要记录我在阅读 《C++ Primer》一书的 Basics 部分时学习到的以前未留意的知识点。主要包含 Chapter 2 Variables and Basic Types
和 Chapter 3 Strings, Vectors, and Arrays
Chapter 1. Variables and Basic Types
String literal
Two string literals taht appear adjacent to one another and taht are separated only by spaces, tabs, or newlines are concatenated into a single literal. We use this form of literal when we need to write a literal that would otherwise be too large to fit comfortably on a single line
字符串字面量可以邻接,编译器在处理的时候会将这些字符串字面量连接起来。
比如下面的代码是合法的
1
2std::cout << "a really, really long string literal"
"that spans two lines" << std::endl;看到这里,我又想起在 C 中也有相同的用法,比如在 Linux kernel 中的
printk
函数,在打印的时候可以指定日志级别,KERN_INFO
,KERN_DEBUG
,KERN_ERR
,KERN_ALERT
等等都是使用#define
指令定义的字符串字面量,在printk
时可以使用这些宏定义。1
printk(KERN_INFO "This is really, really an info from kernl")
List Initialization
Built-in 类型也可以使用 List initialization 进行初始化。List initialization 是 C++ 11 的新特性,可以用作对象的初始化。对于 Built-in 类型的初始化需要注意,不能有精度缺失,否则会报错。
比如下面的初始化均是合法的:
1
2
3
4int units_sold = 0;
int units_sold = {0};
int units_sold{0};
int units_sold(0);而下面的初始化是会报错的:
1
2
3long double ld = 3.1415926536;
int a{ld}, b = {ld}; // error: narrowing conversion required
int c(ld), d = ld; // ok: but value will be truncated可见,在对于 built-in 类型进行 花括号 初始化时,是不能有信息丢失的。:question: 这里是否可以说是不能自动类型转换?(尚未考证)
Variable initialization
对于 built-in类型 来说, 定义在函数体外的变量会被默认初始化为 0 ,而定义在函数体内的变量默认是不会初始化的,这些变量的值是未定义的。
函数体外的变量,一般也就是全局变量,在编译链接的时候会被放在
.bss
段内,所以会被初始化为 0 。而函数体内的变量,可能来自于寄存器,也可能来自栈,无论来自哪里,值都是不确定的。因此,函数体内的变量在声明后比较好的做法是进行初始化。而对于 class 类型 的变量,则由该类的初始化方法决定,对于初始化方法中没有进行初始化的成员变量,将会采取和上述策略进行初始化(即如果不在函数体内,built-in 类型初始化为0,class由类决定;在函数体内,built-in 类型不进行初始化,class依旧由类决定)
比如下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using namespace std;
class Test {
public:
int a;
int *b;
string s;
friend ostream& operator<<(ostream& os, const Test& t) {
os << "a: " << t.a << " p: " << t.b << " s: " << t.s;
return os;
}
};
Test gt;
int main() {
Test lt;
std::cout << gt << endl << lt << endl ;
}在不提供构造方法的时候,
Test gt
和Test lt
都会调用类的默认初始化方法,因此,gt
和lt
都是合法的 object,可以引用。但是由于默认初始化方法不会对成员变量进行初始化,因此这里gt
的几个成员变量将会被初始化,a, b
会被初始化为 0,而s
会调用 string 的构造函数,初始化为""
空字符串。而lt
的几个成员变量则因为在函数体内,a, b
不会被初始化,因此是未定义的值,而string s
一样会调用构造函数,初始化成“”
空字符串。所以,编译这个程序,将可能输出以下的内容:
1
2
3./a.out
# a: 0 p: 0 s:
# a: 1524593168 p: 0x55f8bfb60da7 s:当 Test 提供构造函数,初始化成员变量后,才能保证即使在函数体内,成员变量也是初始化的~
Scope
显示的引用全局作用域可以使用作用域操作符
::
。全局作用域是没有名字的,因此可以通过::test
显示引用全局作用域下的test
对象。1
2
3
4
5
6
7
8
9
10
11
12
using std::cout;
using std::endl;
int test = 10;
int main() {
int test = 5;
cout << test << endl; // 5
cout << ::test << endl; // 10;
}
Pointers & References
指针和引用都是对其他对象的间接引用。
不一样的是:
reference
情根深种,从初始化之后就不能再更改引用的对象;而pointer
在其生命中可以引用不同的对象。reference
从出生就和引用的对象绑定在了一起,它本身并不是一个object,而是一个 alias;pointer
则是自由的,就连出生的时候也不需要指定对象。因此,reference
是不存在地址的,对reference
的寻址,其实都是对reference
引用的对象的寻址。
1
2
3
4
5
6int a = 10; // a is an int type vale
int &b = a; // b is a reference to a
int *c = &a; // c is a pointer to a
int *d = &b; // d is a pointer to a, here &b is &a actually
assert c == d; // ok此外,
reference
在初始化时必须和所引用的对象类型一致,除了有两种例外情况:referece to const
一类的引用。可以引用能转换成这个Reference
类型一致的表达式。比如,1
2
3int a = 10;
const int &b = a; // ok, because `int` can be converted to `const int`
const int &c = 1.23 // ok, because `1.23(double)` can be converted to `const int`reference to base-class
一类的引用。可以使用Pet &
来引用Cat
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
using std::cout;
using std::endl;
using std::string;
class Pet {
public:
string name;
Pet(string name) : name(name) {}
};
class Cat : public Pet {
public:
Cat(string name) : Pet(name){}
};
int main() {
Cat cat("miao~");
Pet &pet = cat;
cout << pet.name << endl; // miao~
}
Const
const
变量默认是只对所定义的那个文件可见的,即默认是 static 的。如果需要在其他文件使用,则在定义时需要添加extern
关键字。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// file1.cpp
const int num = 10;
extern const std::string word = "Hello, world!";
// file2.cpp
extern const int num;
extern const std::string word;
int main() {
std::cout << num << " " << word << std::endl;
return 0;
}
// g++ file1.cpp file2.cpp
// output:
// file2.cpp:(.text+0x6): undefined reference to `num'
// fix by change the definition
// `const int num = 10;` to
// `extern const int num = 10;`
// g++ file1.cpp file2.cpp
// ./a.out
// => 10 Hello, world!正如前面提到的,
reference to const
可以使用和定义reference
类型不一致但是可以通过类型转换得到的对象。比如前面的const int &c = 1.23
。对于这一点,C++ Primer
的作者进行了进一步的解释。编译器在处理类型转换时,需要首先转换成临时变量,再赋值给目标对象的。也就是说对于
const int &c = 1.23
,编译器的处理会分为两步 1. const int db = 1.23; 2. const int &c = db; 由于是reference to const
,因此这里无法更改c
引用的对象。但是,如果允许非 const 的引用类型转换,比如
int &c = 1.23 // error
,分两步就会变成形如int tmp = 1.23; int & c = tmp
; 如此一来,由于c
不是reference to const
, 因此可以改动c
引用的对象的值,于是编译器创建的临时变量的值就会被更改,而这显然和reference
语义不相符。
作者还提到了 top-level const
和 low-level const
, 对于指针来说,top-level const
表示这个 pointer
本身是个 const
, low-level const
则是表示这个 pointer
指向了一个 const
的对象。
当复制一个对象的时候,可以忽略 top-level const
, 因为复制这个动作并不会改变被复制的对象。
1 | int a = 10; |
但是在复制的时候,low-level const
是不可以忽略的,low-level const
的对象不能复制给一个 非 low-level const
的变量。
1 | int a = 10; |
auto
auto
类型修饰的变量必须进行初始化,这个还是比较容易理解的,不进行初始化,编译器没法推导变量类型。此外,
auto
变量推导的类型还有几个需要注意的点:- 当使用
reference
作为 initializer 时,编译器将会使用reference
引用的对象的类型作为auto
推导的类型。 auto
的 initializer 会忽略掉top-level const
, 但是low-level const
不可忽略。auto
不会推导const
类型,如果需要const
的auto
变量,需要显示声明const
, 比如const auto a = 1;
- 当使用
decltype
decltype
是 C++11 的新特性,可以用来定义根据表达式或变量推到的类型的变量。1
2decltype(1) a; // int
decltype(1+4.9) b // double值得注意的是,
decltype
并不会计算表达式,仅仅是编译时进行静态分析。如果
decltype
里是变量的话,将会返回那个变量的类型,包括top-level const
和reference
;如果
decltype
里是表达式的话,将会返回这个表达式生成的对象的类型。1
2
3
4
5int i = 42, *p = &i, &r = i;
decltype(r + 0) b; // int
decltype(*p) c; // error: because *p returns int&
decltype(i) d; // error: int& must be initialized
decltype((i)) e; // error: (i) returns int&, e must be initialized
- constexpr
Chapter 2. Strings, Vectors, and Arrays
Vector initialization
常见的初始化方式就不记录啦,这里记录两个不是很常见的初始化方式
vector<string> vec(10);
比如上面的表达式初始化长度为
10
的向量,每个元素都会被默认初始化。需要注意的是, 如果这个
vector
装载的元素是没有默认初始化方法的话,就不能仅仅定义长度,而必须在明确长度的同时,明确初始化的数据。vector<string> vec{"hello", "test", "world", "!"};
这种初始化方式是
C++11
的新特性,当列表中的元素都是符合vector
装载的类型的时候,会将这些元素挨个添加进入向量。当有元素无法转成vector
装载的类型的时候,比如这里vector<string> vec{10, "test"}
, 就会尝试调用vector<string> vec(10, "test")
的方法来初始化。
Vector range for
当使用
vector
的 range for 语句时,循环体不能改变向量的大小。Iterator
iterator
分为两种,一种是iterator
, 可以读写所引用的对象;另一种是const_iterator
,只能读取引用的对象。在写代码的过程中,一般使用
auto
进行自动类型推导,可以不必了解iterator
的具体细节。1
2
3
4
5string s = "123";
const string cs = "456";
auto it = s.begin(); // string::iterator
auto cit = cs.begin(); // string::const_iterator这里可以看到,
begin()
函数返回的iterator
会根据对象的类型返回iterator
类型或者const_iterator
类型。在C++11
之后,有cbegin()
方法,可以显示的返回const_iterator
类型的迭代器。所有
iterator
支持++
,--
,==
,!=
操作,但是vector
和string
还多支持了一些其它的算术逻辑操作。1
2
3
4
5
6it + n
it - n
it += n
it -= n
it1 - it2
>, >=, <, <= // 用于比较同一个 container 中的迭代器引用的元素顺序先后Arrays
array
和vector
最主要的区别在于,array
的大小是固定的,不可以动态更改。array
可以使用 列表初始化 方式进行初始化。在进行初始化的时候,可以不指定array
的大小, 如果指定了array
的大小,那么初始化的时候,元素数量必须不大于指定的大小。如果用于初始化的元素数量小于指定的大小,不足的部分将会被默认初始化。1
2
3
4int a1[] = {0, 1, 2} // size 为 3 的 array
int a2[3] = {0, 1, 2}
int a3[3] = {} // 相当于 int a3[] = {0, 0, 0}
int a4[3] = {0, 1, 2, 3} // error: 用于初始化的元素数量大于数组大小