《C++ Primer》

文件重定向,可以将文件中的内容作为标准输入 (‘ < ’号),将标准输出写到文件中(‘ > ’号)

1
a.out <input_file >output_file

g++ 版本太老的话,无法直接对C++11标准进行编译,编译时需要加上参数:

1
g++ -std=c++11 a.cpp

变量和基本类型

算术类型包括整型和浮点型,整型又包括字符和布尔类型

如何选择数据类型(p32):

  • 当明确知道数值不为负时,选择unsigned
  • 如果数值超过了int的表示范围,选long long。因为long一般和int具有相同尺寸
  • char在一些机器上是有符号的,再另一些则没有。如果需要使用一个不大的整数,指定它的类型是signed char还是unsigned char
  • 执行浮点数运算选用double。因为float的精度通常不够,而单精度和双精度的计算代价相差无几

当赋给无符号类型一个超出它的表示范围的数时(范围区间以外,负数都超过无符号类型表示范围),结果是初始值对无符号类型表示数值总数取模后所得的余数。取余是向0方向舍入,取模是向-∞方向舍入。

将负数转换为无符号数所得的结果,就可以通过这种计算方法得出。

比如将 -1 赋值给 8bit 的 unsigned char 类型,-1 取模 256 得 -1 余 255,所以赋值之后,结果变成了255.

当赋给有符号类型一个超出它的表示范围的数时,结果是未定义的,程序可能继续工作,也可能崩溃,也可能生成垃圾数据。

切勿混用带符号类型和无符号类型,尤其是做数学运算,会将有符号数转换为无符号数,造成结果出错。

从无符号数中减去一个数,不管这个数是不是无符号数,要保证结果不能是负值。负值转换为无符号数与预期的结果不符。

两个整数相除,结果还是整数,余数部分被自动忽略了。

无符号数不小于0,也会影响到循环退出的条件,要注意!

1
2
extern int i; // 正确用法,声明而非定义
extern int i = 5// 错误用法

复合类型

引用

1
2
3
int ival = 1024
int &ref_val = ival; //ref_val指向ival,是ival的另一个名字
int &ref_val2; //报错,引用必须被初始化

引用和指针都是符合类型,引用本身不是一个对象,所以不能定义引用的引用 (int &(&ref5)=ref1会报错),但是可以定义指针的指针。

引用只能绑定在对象上,不能和字面值或表达式的计算结果绑定:

1
2
3
4
int &ref_val = 10;  // 报错,引用不能和字面值绑定

double a = 3.14;
int &ref_a = a; //报错,引用和绑定对象的类型不匹配

指针 pointer

指针和引用的区别有

  • 指针本身是对象,引用不是对象(一旦定义,就不能绑定到另外的对象),不能定义引用的引用,不能定义引用的指针

  • 指针不需要在定义时赋初始值(但是小心成为野指针!)

&紧随类型名出现,是声明的一部分,表示引用类型;出现在表达式中,是一个取地址符。

*紧随类型名出现,是声明的一部分,表示指针类型;出现在表达式中,是一个解引用符。

空指针的定义方法

  1. int *p1 = 0;

  2. int *p2 = nullptr; C++11标准下,nullptr是一种特殊类型的字面值。

  3. NULL在头文件cstdlib中定义

    1
    2
    #include cstdlib
    int *p3 = NULL;

使用NULL初始化指针和使用0初始化指针是一样的,新标准下,最好使用nullptr,避免使用NULL。

不能将int变量直接赋值给指针,即使int变量的值正好为0也不行。

1
2
3
int *p = 0

if(p) // 不成立

void *是一种特殊的指针类型,可用于存放任意对象的地址。

一条定义语句可以定义出不同类型的变量:

1
int i=1024, *p=&i, &r=i;

对于复合类型的声明,类型修饰符 (*, &) 最好和变量名写在一起,比如:

1
2
int *p = 0;  // 写法1
int* p = 0; // 写法2

对于上面的例子,基本数据类型是int而不是int**只是修饰了p而已,再比如:

1
int* p1=0, p2 = 0;  // p1是指向int的指针,p2是int

引用不是对象,不能定义指向引用的指针,但是存在指针的引用:

1
2
3
4
int i = 5
int *p_i = &i;
int &ref_p = p_i; // 定义对指针的引用出错
int *&ref_p = p_i; // 定义对指针的引用正确

要理解ref_p的类型,需要从右向左阅读ref_p的定义,先是&,说明ref_p是一个引用,然后是*,说明这个引用是对指针的引用,最后是int,表明引用的指针的内容是int类型。

int &ref_p = p_i是对int类型的引用,int *&ref_p = p_i才是对int类型指针的引用。

再来一个例子:

1
2
3
4
5
6
int i = 42;
int *p;
int *&r = p; // r是对指针p的引用

r = &i; // 令p指向i
*r = 0; // 将i的值改为0

const 限定符

const对象一旦创建,其值就不能修改,所以const对象必须初始化:

1
const int k;  //报错,因为没有初始化

在某个文件中定义了const对象,如果想在文件间共享,那么在头文件中这样写:

1
extern const int buffer_size;

const的引用必须是const

1
2
3
const int size = 1024;
int &ref_size = int_size; // 报错,因为不能通过ref_size修改int_size
const int &ref_size = int_size; //正确,const的引用必须是const

之前说“引用只能绑定在对象上,不能和字面值或表达式的计算结果绑定”,对于常量引用有些例外:

1
2
3
4
int i = 42;
const double &r1 = i; // 可以将 const double & 绑定到普通int对象上
const int &r2 = 42; // 正确,r2是一个常量引用,可以和字面值绑定
const int &r3 = r2 * 2; //正确,r3是一个常量引用,可以和表达式的计算结果绑定

const引用的作用是不允许通过该引用修改绑定对象的值:

1
2
int i = 42;
const int &ref_i = i; //不允许通过ref_i修改i的值

指针和 const

指向常量的指针 (pointer to const)

const和指针结合,形成指向常量的指针 (pointer to const):

1
2
3
4
const double pi = 3.14;
double *ptr = &pi; // 错误
const double *ptr = &pi; // 正确
*ptr = 42// 错误,不能修改值

常量指针 (const pointer)

常量指针 (const pointer) 必须初始化:

1
2
3
4
5
int num = 0;
int *const p_num = &num; // p_num将一直指向num

const pi = 3.14;
const double *const p_pi = &pi; // p_pi是一个指向常量对象的常量指针

具体分析的话,还是从右向左阅读。

常量指针,不变的是指针,而不是指针指向的那个值。

顶层 const (top-level const)

用名词顶层 const (top-level const) 表示指针本身是个常量。

用名词底层 const (low-level const) 表示指针所指的对象是个常量。

constexpr 和常量表达式

常量表达式 (const expression) 是指值不会改变并且在编译过程中就能得到计算结果的表达式。

字面值属于常量表达式,用常量表达式初始化的 const 对象也是常量表达式:

1
2
3
4
5
const int max_files = 20;  //max_files是常量表达式
const int limit = max_files + 1; //limit是常量表达式

int staff_size = 29; //staff_size不是常量表达式
const int sz = get_size(); //sz不是常量表达式
constexpr 变量

C++11标准规定,允许将一个变量的类型声明为 constexpr 以便编译器来检查变量的值是否是一个常量表达式:

1
2
3
constexpr int len = 20;
constexpr int max_len = len + 10;
constexpr int sz = size(); //只有当size()是一个constexpr函数是才是一个正确的声明语句

不能使用普通函数作为 constexpr 变量的初始值,新标准允许定义一种特殊的 constexpr 函数,可以初始化 constexpr 变量。

一般来说,如果认定一个变量是常量表达式,那就把它声明成 constexpr 类型。

处理类型

类型别名 (type alias)

typedef

1
2
typedef double wages;
typedef wages base, *p;

basedouble的同义词,pdouble *的同义词。

即:

1
typedef double *p;

p 就是double *

再比如:

1
typedef int arrT[10];

arrT 是一个类型别名,表示的类型是含有10个整数的数组。

别名声明 (alias declaration)

C++11标准规定的新方法

1
2
3
using IT = int;

IT a = 5;

下面两个是等价的:

1
2
typedef int arrT[10];
using arrT = int[10];

arrT 是一个类型别名,表示的类型是含有10个整数的数组。

容易产生误解的地方

现有:

1
typedef char *pstring;  // 注意 char * 是一起的

如果有:

1
const pstring cstr = 0;

该如何理解呢,如果用char *替换pstring,就变成了:

1
const char *cstr = 0;

这样理解,cstr就变成了指向常量的指针,但是这种理解方法是错的,不能进行简单的替换!

pstring是指向char的指针,const pstring表明这个指针是一个常量,cstr是一个常量指针。不能替换再用从右到左法则分析。

在替换之前,pstring的基本数据类型是一个指针,替换之后,const char成了基本数据类型,前者声明了一个指向char的常量指针,替换后则声明了一个指向const char的指针。

auto类型说明符

C++11标准

当表达式返回的值不那么清楚时,可以用auto类型说明符让编译器通过初始值推断变量的类型:

1
auto item = val1 + val2;

关于一些细节和处理顶层、底层const、引用见书 P62

decltype类型指示符

当我们想获取某个类型,来定义一个变量时,可以用到decltype

1
decltype(f()) sum = x;  // sum的类型就是 f() 的返回类型

注意,编译器并不实际调用 f()

关于一些细节和处理顶层、底层const、引用见书 P63

struct

注意最后一个花括号后面加;

struct中的数据没有初始值时,创建对象会默认初始化,为0或空字符串。

预处理器

防止头文件重复包含。

1
2
3
4
5
6
7
8
9
10
#ifndef FILE_NAME_H
#define FILE_NAME_H

#include <string>
struct Student{
std::string name;
unsigned int age = 0;
};

#endif

#ifndef ××#ifdef ××都会判断××有没有被定义过(#define ××),仅当成立时,才进行下面的语句直到#endif

字符串、向量和数组

命名空间的using声明

1
using std::cin;

用了上面的using声明,就可以在声明后的代码中直接用cin来代替std::cin

为了避免冲突,头文件不应包含using声明。

标准库类型string

1
2
#include <string>
using std::string;

初始化string对象

1
2
3
4
5
6
string s1;  // 默认初始化为空字符串
string s2 = s1;
string s2(s1); // 与 s2 = s1 等价
string s3 = "value";
string s3("value"); // 与 s3 = "value" 等价,除了字面值最后的空字符
string s4(n, 'c'); // 初始化为有n个连续的'c'组成的

注意是单引号'c',一个字符类型。

直接初始化和拷贝初始化

  • 直接初始化 (direct initialization)

    1
    string s5("hiya");
  • 拷贝初始化 (copy initialization)

    1
    string s6 = "hiya";

    =进行初始化是拷贝初始化。

String对象上的操作

常用操作有:

1
2
3
4
5
6
7
8
9
10
11
os << s        // 将s写到输出流os中,返回os
is >> s // 从输入流is中读取字符串赋值给s,返回is
getline(is, s) // 从输入流is中读取一行赋值给s, 返回is
s.empty() // s为空返回true,否则返回false
s.size()
s[n]
s1 + s2
s1 = s2 // 用s2的副本代替s1中原来的(全部)字符
s1 == s2
s1 != s2
>, <, >=, <=

从标准输入读取字符串赋值给string时,string对象会自动忽略开头的空白(空格符、换行符、制表符等)并从第一个真正的字符开始读起,直到遇到下一处空白为止

读取未知数量的string对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <string>

int main(int argc, char const *argv[])
{
std::string word;

// 读取不定数量的输入
// 当遇到文件结束符或者无效输入,循环结束
// 文件结束符(ctrl+z in Windows, ctrl+d in UNIX)
while(std::cin >> word){
std::cout << word << std::endl;
}

return 0;
}

std::cin >> a 可以判断输入是否合理,可以用在if、for、while里面。如果是读取未知数量的int类型,字母就是无效输入。

如果想保留输入的空白符号,应该用getline代替>>运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
#include <string>


int main(int argc, char const *argv[])
{
std::string name;

std::cout << "Please, enter your full name: ";
std::getline(std::cin,name);
std::cout << "Hello, " << name << "!\n";

return 0;
}

getline的规则是遇到换行符才结束读取,返回读取结果,如果一开始就输入换行符,得到的结果就是个空string。

字面值和string对象相加时,要注意,不能把两个字符字面值相加

1
2
3
4
5
string s1 = "hello";
string s2 = s1 + "world" //正确,string对象加上字面值
string s3 = "hello" + ", world" //错误,不能把两个字符字面值相加
string s4 = s1 + "," + " world" //正确
string s5 = "hello" + "," + s1 //错误

字符串字面值与string是不同的类型。

处理string对象中的字符

cctype

cctype头文件中定义了一组标准库函数处理字符,列举一些:

1
2
3
4
5
6
7
8
char c;

isalnum(c) //判断是否是字母或数字
isalpha(c) //判断是否是字母
isdigit(c) //判断是否是数字
isspace(c) //判断空格
isxdigit(c) //判断是否十六进制数
tolower(c) //如果c是大写字母,就变成小写字母输出,否则原样输出

注意,cctype头文件的包含方法是#include <cctype>,其内容和C语言的标准库ctype.h内容是一样的。C++将兼容的C标准库头文件重新命名(去掉.h后缀,前面又加了一个c)。

基于范围的for语句

C++11新标准提供了范围for(range for)语句:

1
2
3
4
5
string s("some string");

for(auto c : s){
cout << c << endl;
}

注意,每次循环,str1中的字符被拷贝c.

如果想用for循环修改str1的内容,必须把循环变量定义成引用类型:

1
2
3
4
5
6
7
string s("some string");

for(auto &c : str1){
c = toupper(c);
}

cout << s << endl;

s.size()的返回值和string的索引值都是string::size_type类型,这种类型是无符号数,不会成为负值。如果n是一个带符号类型的值,s[n]会自动将索引值n转换为string::size_type表达的无符号类型。

标准库类型vector

1
2
#include <vector>
using std::vector;

vector 也被称作容器 (container),是一个类模板

对于类模板来说,可以指定模板实例化成什么样的类(在模板名字后面的尖括号中定义):

1
2
3
vector<int> ivec;
vector<Student> student_vec;
vector<vector<string>> file;

vector 中对象的类型相同。

在早期的C++标准中,如果 vector 的类型还是 vector(或者其他模板类型),需要多加一个空格,类似于:

1
vector<vector<int> >

C++11则不必加这个空格。

定义和初始化vector对象

1
2
3
4
5
6
7
vector<T> v1  //空vector, 由T执行默认初始化
vector<T> v2(v1) //v2包含v1所有元素的副本
vector<T> v2=v1 //跟上面等价
vector<T> v3(n, val) //v3包含了n个重复的元素(val)
vector<T> v4(n) //v4包含了n个重复地执行了值初始化的对象
vector<T> v5{a, b, c, ...} //v5包含了初始值个数的元素,每个元素被赋予相应的初始值
vector<T> v5={a, b, c, ...} //跟上面等价

当对vector进行拷贝赋值时,两个vector所含对象的类型要相同。

列表初始化vector对象

C++11标准:

1
2
3
vector<string> s = {"a", "an", "the"};
vector<string> s1{"a", "an", "the"};
vector<string> s2("a", "an", "the"); //用圆括号是错的

创建指定数量的元素

1
vector<string> s3(10, "hi");

值初始化

1
2
vector<int> ivec(10);  //10个元素,每个都初始化为0
vector<string> svec(10); //10个元素,每个都初始化为空字符串

初值由 vector 中元素对象的类型决定,如果类型不支持默认初始化,就必须提供初始值。

花括号和圆括号

1
2
3
4
5
vector<int> v1(10);  //10个元素,每个值都是0
vector<int> v2{10}; //1个元素,值为10

vector<int> v3(10, 1); //10个元素,每个值都是1
vector<int> v4{10, 1}; //2个元素,值分别为10,1

花括号表示列表初始化(list initialize)vector 对象。

如果初始化使用了花括号,但是花括号中的值不能进行初始化,编译器会尝试用默认值初始化 vector 对象:

1
2
3
4
5
vector<string> s1{"hi"};  //列表初始化
vector<string> s2("hi"); //错误,不能使用字符串字面值构建vector对象

vector<string> s3{10}; //不能进行列表初始化,编译器采用默认值初始化,s3中有10个默认初始化的元素
vector<string> s4{10, "hi"} //不能进行列表初始化,s4中有10个值为"hi"的元素

向vector添加元素

利用 vector 的成员函数push_back,从字面意思很好理解,将新元素压到原 vector 尾部。

vector 对象能够高效增长,所以在定义 vector 对象时设置其大小也就没什么必要了,事实上如果这么做效果可能更差。只有一种情况例外,就是 vector 对象中所有元素的值都一样(类似于vector<int> v3(10, 1))。

如果在循环体内部包含有向vector添加元素的语句,则不能使用范围for循环。

其他vector操作

1
2
3
4
5
6
7
v.empty()  //如果v中不含有任何元素,返回true
v.size() //返回v中元素的个数
v1 = v2 //用v2中的元素拷贝替换v1中的元素
v1 = {a, b, c, ...} //用列表中的元素拷贝替换v1中的元素
v1 == v2 //仅当元素数量和对应位置元素值都相同,才返回true
v1 != v2
>, >=, <, <=

也可以用范围for语句处理 vector 中元素:

1
2
3
4
vector<int> v{1,2,3,4,5};
for(auto &i : v){
i *= i;
}

v.size()返回的是由 vector 定义的 size_type 类型,如:vector<int>::size_type.

迭代器

所有标准库容器都可以使用迭代器(比如 vector ),但是其中只有少数几种才同时支持下标运算符。每个容器类定义了一个名为 iterator 的类型,该类型支持迭代器概念所规定的一套操作。

严格来说,string 对象不属于容器类型,但是它支持很多与容器类型类似的操作(也支持迭代器)。

使用迭代器可以访问某个元素,迭代器也能从一个元素移动到另一个元素。

使用迭代器

有迭代器的类型同时拥有返回迭代器的成员。比如,这些类型都有名为 beginend 的成员:

1
auto b = v.begin(), e = v.end();

begin 成员负责返回指向第一个元素(对于 vector )或第一个字符(对于 string )的迭代器。

end 成员负责返回指向容器(或 string 对象)尾元素下一位置 (one past the end) 的迭代器。也就是说,该迭代器指示的是容器的一个本不存在的 “尾后 (off the end)” 元素end 成员返回的迭代器常被称作尾后迭代器 (off-the-end iterator) 或者简称为尾迭代器 (end iterator).

特殊情况下,如果容器为空,则 beginend 返回的是同一个迭代器,都是尾后迭代器

迭代器的类型

就像不知道 stringvectorsize_type 成员到底是什么类型一样,我们也不知道(不用知道)迭代器的精确类型,所以使用 auto

实际上,拥有迭代器的标准库类型使用 iteratorconst iterator 来表示迭代器的类型:

1
2
3
4
5
vector<int>::itreator it;  //it能读写vector<int>的元素
string::iterator it2; //it2能读写string对象中的字符

vector<int>::const_itreator it3; //it3只能读元素,不能写
string::const_iterator it4; //it4只能读字符,不能写

如果 vector 对象或 string 对象是一个常量,那么只能用 const_iterator.

beginend 返回的具体类型由对象是否是常量决定,如果对象是常量,则他俩返回的是 const_iterator,否则返回iterator

1
2
3
4
5
vector<int> v;
const vector<int> cv;

auto it1 = v.begin(); //it1的类型是vector<int>::itreator
auto it2 = cv.begin(); //it2的类型是vector<int>::const_itreator

有些时候,对象只需读操作,不需要写操作,最好用 const_itreator,为了更方便的得到这种类型,C++11标准引入了两个新函数 cbegincend

1
auto it3 = v.cbegin();  //it3的类型是vector<int>::const_itreator

标准容器迭代器的运算符

1
2
3
4
5
6
7
*iter  //返回迭代器iter所指元素的引用
iter->mem //解引用iter并获取该元素的名为mem的成员
(*iter).mem //跟上面等价
++iter //令iter指向容器中的下一个元素
--iter //令iter指向容器中的上一个元素
iter1 == iter2 //判断两个迭代器是否相等,如果他们指向同一个元素或他们是同一个容器的尾后迭代器,则相等
iter1 != iter2

迭代器运算 (iterator arithmetic)

迭代器的递增运算令迭代器每次移动一个元素,所有标准库容器都有支持递增运算的迭代器。

stringvector 的迭代器提供了更多额外的运算符,可以使迭代器的每次移动跨过更多的元素,也支持迭代器进行关系运算:

1
2
3
4
iter + n
iter - n
iter1 - iter2 //二者必须是同一个vector中的迭代器
>, >=, <, <= //二者必须是同一个vector中的迭代器, 指向位置靠前的迭代器小与指向靠后位置的迭代器

指向某 vector 中间位置的元素:

1
auto mid = v.begin() + v.size()/2

两个迭代器(指向同一 vector)相减,返回类型为 difference_type 的带符号整型数(可正可负)。

数组

数组也是存放类型相同的对象的容器。与 vector 不同的是,数组的大小是固定的。

定义和初始化内置数组

数组是一种复合类型,数组的声明形如 a[d], d是数组的维度,必须大于 0,编译时维度应该是已知的,所以,维度必须是一个常量表达式:

1
2
3
4
5
6
7
unsigned cnt = 42;  //不是常量表达式
string bad[cnt]; //错误:cnt不是常量表达式

constexpr unsigned sz = 42; //常量表达式
int *parr[sz]; //含有42个整型指针的数组

string strs[get_size()]; //当get_size()是constexpr函数时正确,否则错误

默认情况下,数组元素被默认初始化。

vector 一样,数组的元素应为对象,不存在引用的数组。

显示初始化数组元素

对数组的元素进行列表初始化时,允许忽略数组的维度,编译器会根据初始值的数量推测出来。如果还是指明了维度,那么初始值的总数量不应该超出维度的大小,如果维度比初始值数量大,那么剩下的元素还是默认初始化。

1
2
int a1 = {0, 1, 2};
int a2[5] = {3, 4, 5}; //等价于 a2 = {3, 4, 5, 0, 0}

字符数组的特殊性

字符串字面值的结尾处还有一个空字符,这个空字符也会被拷贝到字符数组中:

1
2
3
4
char a1[] = {'C', '+', '+'};  //列表初始化,没有空字符
char a2[] = {'C', '+', '+', '\0'}; //列表初始化,含有显式的空字符
char a3[] = "C++"; //自动添加表示字符串结束的空字符
const char a4[6] = "Daniel"; //错误,没有空间可存放空字符

a1 的维度是3,a2a3 的维度都是4. a4的大小要至少是7,因为要存放空字符\0.

不允许拷贝和赋值

不能将数组的内容拷贝给其他数组做初始值,也不能用数组为其他数组赋值:

1
2
3
int a[] = {0, 1, 2};
int a2[] = a; //错误
a2 = a; //错误

一些编译器支持数组的赋值,这就是所谓的 编译器扩展

复杂的数组声明

1
2
3
4
int *ptrs[10];  // ptrs是含有10个整型指针的数组
int &refs[10]; // 错误,不存在引用的数组
int (*Parray)[10] = &arr; // Parray是指针,指向一个含有10个整数的数组
int (&arrRef)[10] = arr; // arrRef引用一个含有10个整数的数组

默认情况下,类型修饰符从右向左依次绑定。括号具有优先级:

1
int *(&arry)[10] = ptrs;  // array是数组的引用,该数组含有10个指针

访问数组元素

string, vector 一样,数组元素也能用范围for语句和下标运算符来访问。

在使用数组下标的时候,通常将其定义为 size_t 类型, size_t 是一种机器相关的无符号类型,它被设计的足够大以便表示内存中任意对象的大小,在 cstddef 头文件(C中为 stddef.h )中定义了 size_t 类型。

指针和数组

C++中,编译器一般会把数组转换为指针。

1
2
string nums[] = {"one", "two", "three"};
string *p = &num[0]; // p指向nums的第一个元素

在用到数组名字的时候,编译器会自动将其替换为一个指向数组首元素的指针

1
string *p2 = nums;  // 等价于p2 == &num[0]

指针也是迭代器

stringvector 的迭代器支持的运算,数组的指针全都支持。

可以获取数组尾元素之后的那个并不存在的元素的地址作为尾后指针:

1
int *e = &arr[10];  // arr中共有10个元素,e是指向arr尾元素下一位置的指针

和尾后迭代器一样,尾后指针不指向具体的元素。

标准库函数 begin 和 end

这两个函数定义在 iterator 头文件中。

通过计算得到尾后指针的方法容易出错,C++11引入了 begin 和 end 两个函数。

因为数组不是类类型,因此这两个函数不是成员函数,使用方式是将数组作为参数:

1
2
3
int ia[] = {0, 1, 2, 3};
int *b = begin(ia); // 指向ia首元素的指针
int *e = end(ia); // 指向ia尾元素的下一位置的指针

注意,两个指针相减的结果是一种名为 ptrdiff_t 的标准库类型,和 size_t 一样,ptrdiff_t也定义在cstddef 头文件中,是机器相关的带符号类型(size_t 是无符号类型)。

下标和指针

对数组执行下标运算其实是对指向数组元素的指针执行下标运算

1
int i = ia[2];

上面这行代码等价于下面两行代码

1
2
int *p = ia;
int i = *(ia+2);

只要指针指向的是数组中的元素,都可以执行下标运算:

1
2
3
int *p = &ia[2];
int j = p[1]; // p[1]等价于*(p+1), 就是ia[3]指向的那个元素
int k = p[-2]; // k是ia[0]指向的那个元素

标准库类型 string, vector 也能执行下标运算,但是它们使用的下标必须是无符号类型,这点和内置的(数组)下标不同。

C 风格字符串

C++中最好还是不要使用 C 风格字符串(char s[]),用标准库string会更安全。

与旧代码的接口

混用 string 对象和 C 风格字符串

见书 P.111

使用数组初始化 vector 对象

不允许用数组对另一个内置类型的数组进行赋值。可以使用数组来初始化 vector 对象,只需要指明要拷贝区域的首元素地址和尾后地址:

1
2
int arr[] = {0, 1, 2, 3, 4};
vector<int> ivec(begin(arr), end(arr));

也可以用数组的一部分来初始化 vector 对象:

1
2
int arr2[] = {0, 1, 2, 3, 4};
vector<int> sub_vec(arr2+1, arr2+4);

sub_vec 中有三个元素,值分别和arr2[1], arr2[2], arr2[3]相同。

现代的C++程序应尽量使用 vector 和迭代器,避免使用内置数组和指针;应当尽量使用 string,避免使用C风格的基于数组的字符串。

多维数组

严格来说,C++中没有多维数组,通常所说的多维数组其实是数组的数组。

1
int ia[3][4];  //大小为3的数组,每个元素是含有4个整数的数组

对于二维数组,常把第一个维度称为行,第二个维度称为列。

多维数组的初始化

允许使用花括号对多维数组初始化:

1
2
3
4
5
int ia[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};

其中内层嵌套的花括号可以省略,下面的做法与上面等效

1
int ia[3][4] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

如果仅仅想初始化每一行的第一个元素(这个时候就不能省略内层的花括号):

1
2
3
4
5
int ia[3][4] = {
{0},
{4},
{8}
};

其他未列出的元素执行默认初始化。

如果有:

1
int ia[3][4] = {0, 1, 2, 3};

这个时候,初始化的是第一行的4个元素。

多维数组的下标引用

1
2
int ia[3][4];
int (&row)[4] = ia[1]; //row是ia[1]这个数组的引用

使用范围for语句处理多维数组

可以使用两个普通的 for 循环来处理二维数组,多维同理。由于C++11增加了范围 for 语句,可以这样对二维数组进行遍历:

1
2
3
4
5
6
7
size_t cnt = 0;
for(auto &row : ia){
for(auto &col : row){
col = cnt;
++cnt;
}
}

上面用了两个引用类型,实际上,如果使用范围 for 语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。如果不用引用类型:

1
2
3
4
5
6
7
8
// ia是二维数组3*4大小

for(auto row : ia){
for(auto col : row){
col = cnt;
++cnt;
}
}

程序将无法通过编译,原因:对于外层循环来说,row的类型本应是是数组指针 ( int (*row)[4] ),但是去掉了引用,编译器初始化row的时候会自动将数组形式的元素转换成指向该数组内首元素的指针,于是,row的类型变成了int * (假设 ia 中元素是整型),在接下来的循环中,编译器试图在一个 int * 内遍历,就会不合法。深层原因就是下面“指针和多维数组”中的内容。

指针和多维数组

1
2
3
int ia[3][4];
int (*p)[4] = ia; // p指向含有4个整数的数组
p = &ia[2]; // p指向第三行的数组(ia的尾元素)

注意上面第二行代码中,圆括号不可少:

1
2
int *ip[4];  // ip指向--整型指针的数组
int (*ip)[4] = ia; // ip指向--含有4个整数的数组

类型别名简化多维数组的指针

1
2
3
4
5
6
7
8
using int_array = int[4];  // C++11 标准
typedef int int_array[4]; // 与上面等价

int ia[3][4];

for(int_array *p = ia; ...; ...){
...
}

表达式

一元运算符:作用于一个对象,如 取地址&

二元运算符:作用于两个对象,如==

三元运算符:作用于三个对象,仅有一个。

函数也是一种特殊的运算符,它对运算对象的数量没有限制。

一些符号既能做一元运算符(*表示解引用),也可以做二元运算符(*表示乘法)。

C++语言没有明确大多数二元运算符的求值顺序,即:

1
f() * g();

无法确定f函数和g函数哪一个先执行。

让代码更加简洁:

1
cout << *iter++ << endl;

1
2
cout << *iter << endl;
++iter;

更好。

ptr->等价于(*ptr).mem,注意优先级。

条件运算符:

1
cond? expr1 : expr2;

如果cond为1,对expr1求值并返回该值,否则对expr2求值并返回该值。注意,并不会expr1和expr2都执行。

sizeof运算符:

所得值是一个size_t类型的常量表达式。

运算符的运算对象有两种形式:

1
sizeof

举例:

1
2
3
int *p;
sizeof p; // 指针所占空间的大小
sizeof *p; // p所指类型的空间大小,即sizeof(int), 指针需要有效

对数组执行sizeof运算得到整个数组所占空间的大小;

string对象或vector对象执行sizeof运算只返回该类型固定部分的大小,不会计算对象中的元素占用了多少空间。

类型转换

隐式转换

p141

显示转换

强制类型转换

函数

参数传递

使用引用避免拷贝

如果不需要修改参数,可以定义成常量引用。

const形参和实参

当用实参初始化形参时会忽略掉顶层const(当形参有顶层const时,传给它常量对象或者非常量对象都是可以的),但是,这样产生了问题:

1
2
3
4
5
6
7
8
void fcn(const int i)
{
...
}
void fcn(int i) // 错误,重复定义了fcn
{
...
}

定义相同名字的函数需要不同的形参列表。但是因为顶层const被忽略了,所以上面两个函数的参数可以完全一样,因此出现了重复定义的错误。

尽量使用常量引用

如果定义函数:

1
void find_char(string &s);

这样调用会发生错误:

1
find_char("Hello");

需要将函数的定义改为:

1
void find_char(const string &s);

数组形参

数组有两个特殊性质:

  • 不允许拷贝数组
  • 数组会被转换成指针

下面三个函数的声明是等价的:

1
2
3
void print(const int*);
void print(const int[]);
void print(const int[10]);

数组的大小对函数的调用没有影响。

一开始函数不知道数组的确切尺寸,调用者应该为此提供一些额外的信息。管理指针形参有三种常用的技术:

  1. 使用标记指定数组长度

    要求数组本身包含一个结束标记,比如C风格字符串最后的空字符 (\0).

  2. 使用标准库规范

    传递指向数组首元素和尾后元素的指针:

    1
    2
    3
    4
    5
    6
    7
    void print(const int *beg, const int *end):
    {
    while(beg != end)
    {

    }
    }

  3. 显示传递一个表示数组大小的实参

数组引用形参

如果函数需要改变元素值,就要将形参定义成数组的引用:

1
2
3
4
5
6
7
void print(int (&arr)[10]):
{
for(auto elem : arr)
{

}
}

&arr两端的括号不可少:

1
2
int &arr[10];  //引用的数组(数组里面存放的是引用)
int (&arr)[10]; //数组的引用

这样的缺陷是只能将函数作用于大小为10的数组,如果想作用于任意大小的数组,就需要 模板

传递多维数组

对于多维数组来说,处理的是数组的数组,所以首元素本身就是一个数组,指针就是一个指向数组的指针。数组的第二维和后面所有维度的大小都是数组类型的一部分,不能省略。比如要传递一个二维数组:

1
2
3
4
void print(int (*matrix)[10], int row_size)
{

}

matrix指向数组的首元素,该数组的元素是由10个整数构成的数组。注意,这个时候并不知道第一个维度是多少,但后面的维度必须明确(知道第二个维度是10)。

也可以使用数组的语法定义函数,但是第一个维度会被编译器忽略,所以最好不写第一个维度:

1
2
3
4
void print(int matrix[][10], int row_size)
{

}

含有可变形参的函数

p197

返回类型和 return 语句

无返回值函数

对于 void 类型函数,可以不写 return (在函数最后会隐式执行),也可以在需要提前退出函数的地方写 return

有返回值函数

不要返回局部对象的引用或指针

函数完成后,它所占用的存储空间也随之被释放

列表初始化返回值

C++11标准规定,函数可以返回花括号包围的值的列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vector<string> process()
{
...
// expected和actual是string对象

if (expected.empty())
{
return {};
}
else if (expected == actual)
{
return {"functionX", "okay"};
}
else
{
return {"functionX", expected, actual}
}
}

return中的列表用来对表示函数返回的临时量进行初始化(上面的例子是初始化vector<string>对象)。

返回数组指针

数组不能拷贝,所以函数不能返回数组,但是函数可以返回数组的指针或引用。从语法上来说,定义过程比较繁琐,但是有一些方法可以简化这些任务,其中最直接的方法是使用类型别名

1
2
3
4
typedef int arrT[10];
using arrT = int[10];

arrT* func(int i);

arrT 是一个类型别名,表示的类型是含有10个整数的数组。

想要在声明func()时不使用类型别名,我们就必须牢记数组的维度,类似于:

1
2
int arr[10];
int (*p)[10] = &arr;

应该这样声明func()

1
int (*func(int i))[10];

C++11标准中,还可以使用 尾置返回类型 (trailing return type) 来简化 func() 的声明:

1
auto func(int i) -> int(*)[10];

尾置返回类型跟在形参列表后面,并以 -> 符号开头,在本应该出现返回类型的地方放置一个 auto

任何函数的定义都能使用尾置返回,但是这种形式对于返回类型比较复杂的函数最有效。

如果我们知道函数返回的指针将指向哪个数组,还可以使用 decltype 关键字声明返回类型:

1
2
3
4
5
6
int odd = {0, 1, 2, 3, 4}

decltype(odd) *arrPtr()
{
return &odd;
}

函数重载

重载(overload)函数:同一作用域内的几个函数名字相同但形参列表(形参数量、类型)不同。

main函数不能重载。

不允许两个函数除了返回类型外其他所有要素都相同。

重载和const形参

一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开:

1
2
3
4
5
void func1(int a);
void func1(const int a); //错误,重复声明

void func2(int*);
void func2(int* const); //错误,重复声明

如果形参是某种类型的指针或引用,则通过区分其指向的是常量对象还是非常量对象可以实现函数重载,此时的const是底层的:

1
2
3
4
5
void func3(int*);
void func3(const int*); //新函数

void func4(int&);
void func4(const int&) //新函数

内联函数

内联(inline)函数可以避免函数调用的开销。

在普通函数的返回类型前面加上关键字inline,这样就可以将它声明成内联函数。

内联函数只是向编译器发出的一个请求,编译器可以选择忽略这个请求。一般来说,内联机制用于优化规模较小,流程直接,频繁调用的函数。

constexpr函数

constexpr函数指能用于常量表达式的函数。

定义constexpr函数时:

  1. 函数的返回类型及所有形参的类型都得是字面值类型。
  2. 函数体中必须有且只有一条return语句。

执行初始化任务时,编译器把对constexpr函数的调用替换成其结果值。为了能在编译过程中随时展开,constexpr函数被隐士地指定为内联函数。

constexpr函数中可以包含 空语句、类型别名以及using声明 这样的不执行任何操作的语句。

内联函数和constexpr函数通常定义在头文件中。如果内联函数定义在某一个源文件中,编译过程会将这个内联函数替换掉,其他源文件就找不到这个内联函数了。和其他函数不一样,内联函数和constexpr函数可以在程序中多次定义。

调试帮助

assert预处理宏

1
2
3
#include <cassert>

assert(expr);

如果expr的值为 ,则输出信息,终止程序。

NDEBUG预处理变量

通过名为 NDEBUG 的预处理变量,可以使 assert 失效,关闭调试状态,有两种方法:

  1. #define 定义 DEBUG

  2. 很多编译器都提供命令行选项使我们可以定义预处理变量:

    1
    CC -D NDEBUG main.c # use /D with the Microsoft compiler

    这条命令的作用等同于在 main.c 文件的一开始写的#define NDEBUG

还可以将 NDEBUG 用于自己编写的调试代码:

1
2
3
4
5
6
7
8
void print(const int ia[], size_t size)
{
#ifndef NDEBUG
cerr << __func__ << ": array size is " << size << endl;
#endif

...
}

使用变量__func__输出当前调试的函数的名字。编译器为每个函数都定义了__func__,它是 const char 类型的一个静态数组,存放函数的名字。除此之外,预处理器还定义了4个对于程序调试很有用的名字:

  1. __FILE__存放文件名的字符串字面值。
  2. __LINE__存放当前行号的整型字面值。
  3. __TIME__存放文件编译时间的字符串字面值。
  4. __DATE__同上,存放编译日期。

函数指针

想要声明一个指向函数的指针,只需要用指针替换函数名即可:

1
bool (*pf)func(int i);  //注意 *p 两端的括号不可少

使用函数指针

当把函数名作为一个值使用时,该函数自动的转换成指针。

1
2
pf = lengthCompare;
pf = &lengthCompare;

lengthCompare是某个函数名,上面两行等价。

用指向函数的指针调用该函数:

1
2
bool b1 = pf(5);
bool b2 = (*pf)(5)

上面这两种调用方式等价。

指向不同函数类型的指针间不存在转换规则,但是仍可以赋值 nullptr 或者值为0的整型常量表达式,表示该指针没有指向任何一个函数:

1
2
pf = 0;  // 正确,pf现在不指向任何函数
pf = someFunc; // 错误,返回类型不匹配

剩下更多内容见p221


----------over----------