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 &来引用Cat1
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: 用于初始化的元素数量大于数组大小


