《Cpp primer》 笔记
C++真有趣(狗头)
C++入门
- 基础知识
一、简单的C++程序
- 必须有一个命名为 main 的函数,用于系统运行C++程序,其返回值必须为 int
- 函数的定义:返回类型、函数名、形参列表(可以为空)、函数体
1
2
3
4int main()
{
return 0;
} - int 类型是一种内置类型,即语言自身定义的类型
- C++ 是需要以 分号(;)作为结尾的
- istream:输入流
- ostream:输出流
- 流:一个流就是一个字符序列,是从IO设备读出或写入IO设备的,想表达随着时间的推移,字符是顺序生产或消耗的。
- 缓冲区(buffer):一个存储区域,用于保存数据。IO设施通常将输入(或输出)数据保存在一个缓冲区中,读写缓冲区的动作与程序中的动作是无关的。我们可以显式地刷新输出设备。默认情况下,读 cin 会刷新 cout;程序非正常终止时也会刷新 cout
- 4个标准库定义的IO对象
- cin:标准输入,istream类型
- cout:标准输出,ostream类型
- cerr & clog:标准错误,ostream类型,写到 cerr 的数据是不缓冲的
- 一个简单的IO程序:
1
2
3
4
5
6
7
8
9
10
int main()
{
std::cout << "Enter two number:" << std::endl;
int v1 = 0, v2 = 0;
std::cin >> v1 >> v2;
std::cout << "The sum of " << v1 << " and " << v2
<< " is " << v1 + v2 << std::endl;
return 0;
} - 尖括号(<>)中的名字指出了一个头文件,#include指令和头文件的名字必须写在同一行
- 输出运算符:<<,在标准输出上打印消息,接受两个对象,左侧的运算对象必须是一个 ostream 类型的对象,右侧的运算对象是要打印的值
- 输入运算符:>>,输入运算符返回其左侧运算对象作为其计算结果
- 命名空间:标准库定义的所有名字都在命名空间 std 中
- 使用 作用域运算符(::) 来指出我们想使用定义在命名空间中的名字,命名空间名 :: 定义名
- ++:前缀递增运算符
- 文件结束符:Windows中是Ctrl+Z,UNIX和Mac OS X中是Ctrl+D
- std::cout 和 printf
- 禁止 std::cout 和 printf 混用,在多线程环境下可能会导致 coredump
- std::cout 有缓冲区,printf 没有缓冲区
- printf 在对标准输出做任何处理前先加锁
- std::cout 在实际向标准输出打印时方才加锁
- 类:通过定义一个类(class)来定义自己的数据结构
- 头文件:使用 .h 作为头文件的后缀
- 包含来自标准库的头文件时,应该使用尖括号(<>)包围头文件名,对于不属于标准库的头文件,则使用双引号(“ “)包围
- 成员函数:定义为类的一部分的函数,也被称为方法
- C++标准库定义的名字在命名空间 std 中
二、变量和基本类型
- 除去布尔型和扩展的字符型,其余整型可以划分为 带符号的 和 无符号的 两种,带符号类型可以表示正数、负数或 0,无符号类型则仅能表示大于0的值。
- int、short、long和long long 都是带符号的,可在这些类型名前添加 unsigned 就可以得到无符号类型
- 选择类型的经验准则:
- 当明确知晓数值不可能为负数时,选用无符号类型
- 使用int执行正数运算。整形用int,不够直接用long long
- 在算数表达式中不要使用 char 或 bool,只有在存放字符或布尔值时才使用,因为 char 在一些机器上是有符号的,而在另一些机器上又是无符号的,所有如果使用 char 进行运算特别容易出问题。如果需要使用一个不大的整数,那么明确指定它的类型是 signed char 或 unsigned char
- 执行浮点数运算选用 double,因为双精度浮点数精度大于单精度浮点数,且计算代价相差无几;long double 的消耗过大且精度在一般情况下没必要
- 0 转换为布尔类型时为 false,非0 的值转换为布尔值时均为 true
- 转义序列均以反斜线作为开始
- 如果反斜线(\)后面跟着的八进制数字超过3个,只有前3个数字与反斜线(\)构成转义序列
- 指针字面值:nullptr
- 变量:提供一个具有名字、可供程序操作的存储空间
- 对C++程序员来说,“变量(variable)” 和 “对象(object)” 一般可以互换使用
- 对象:指一块能存储数据并具有某种类型的内存空间
- 初始化不是赋值,初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象当前的值擦除,用一个新的值来替代
- 列表初始化:C++11新标准中使用花括号来初始化变量,该方法使得初始化更安全,避免了类型转换导致的精度损失的危险
- 定义在函数内的内置类型的对象,如果没有初始化,则其值未定义
- 类的对象如果没有显示地初始化,则其值由类决定
- 分离式编译:该机制允许将程序分割为若干个文件,每个文件可被独立编译
- 声明:规定了变量的类型和名字,使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明
- 定义:负责创建于名字关联的实体,除了规定变量的类型和名字,还申请存储空间,也可能会为变量赋一个初始值
- extern:将该关键字添加在变量名前,并且不要显式地初始化变量,就可以只声明而非定义它
- 任何包含了显式初始化的声明即成为定义
- 变量能且只能被定义一次,但是可以被多次声明
- 如果要在多个文件中使用同一个变量,那么变量的定义必须且只能出现在一个文件中,其他使用该变量的文件必须对其声明,却绝不能重复定义
- C++为标准库保留了一些名字,用户自定义标识符时不能出现连续两个下划线,也不能以下划线紧连大写字母开头,定义在函数体外的标识符不能以下划线开头
- 作用域:C++中大多数作用域都是以花括号分隔,在其中名字有其特定的含义
- 作用域能彼此包含,被包含(或者说被嵌套)的作用域称为 内层作用域,包含着别的作用域的作用域称为 外层作用域
- 允许在内层作用域中重新定义外层作用域已有的名字
- 复合类型:基于其他类型定义的类型
- 引用(左值引用)
- 引用(左值引用):为对象起了另一个名字,引用类型(refers to)引用另外一种类型,通过将声明符写成 &d 的形式来定义引用类型,其中 d 是声明的变量名
- 引用必须初始化:定义引用时,程序会把引用和它的初始值 绑定 在一起,而不是将初始值直接拷贝给引用,一旦初始化完成,引用将和它的初始值对象一直绑定在一起且无法重新绑定到另外一个对象上
- 引用即别名:引用并非对象,相反的,它只是一个已经存在的对象所起的另外一个名字
- 允许在一条语句中定义多个引用,其中每个引用标识符都必须以符号 & 开头,且引用类型的初始值必须是一个对象,二者类型也必须相同
- 引用(左值引用):为对象起了另一个名字,引用类型(refers to)引用另外一种类型,通过将声明符写成 &d 的形式来定义引用类型,其中 d 是声明的变量名
- 指针
- 指针:是“指向”另外一种类型的复合类型,与引用类似,指针也实现了对其他对象的间接访问
- 指针与引用的不同点:
- 指针本身就是一个对象,允许对指针赋值和拷贝,而且在指针的生命周期内它可以先后指向几个不同的对象
- 指针无须再定义时赋初值
- 在块作用域内定义的指针如果没有被初始化,也将拥有一个不确定的值
- 定义指针类型的方法是将声明符写成 *d 的形式,其中 d 是变量名
1
2int *ip1, *ip2; //ip1和ip2都是指向int型对象的指针
double dp, *dp2; //dp2是指向double型对象的指针,dp是double型对象 - 取地址符(&):获取指针存放某个对象的地址
1
2
3
4
5
6double dval;
double *pd = &dval; //正确:初始值是double型对象的地址
double *pd2 = pd; //正确:初始值是指向double对象的指针
int *pi = pd; //错误:指针pi的类型和pd的类型不匹配
pi = &dval; //错误:试图把double型对象的地址赋给int型指针 - 指针值(即地址)应属于下列四种状态之一:
- 指向一个对象
- 指向紧邻对象所占空间的下一个位置
- 空指针,意味着指针没有指向任何对象
- 无效指针,也就是上述情况之外的其他值
- 利用指针访问对象
- 如果指针指向一个对象,则允许使用 解引用符(操作符*) 来访问该对象
- 给解引用的结果赋值,实际上也是给指针所指的对象赋值
1
2
3
4
5
6int ival = 42;
int *p = &ival; //P存放着变量ival的地址,或者说p是指向变量ival的指针
std::cout << *p << std::endl; //由符号*得到指针p所指的对象,输出42
*p = 0; //由符号*得到指针p所指向的对象,即可经由p为变量ival赋值
std::cout << ival << std::endl; //输出0
- 空指针:不指向任何对象
- 生成空指针的方法:
1
2
3
4int *p1 = nullptr; //等价于 int *p1 = 0; 这是得到空指针最好的办法,也是C++11新标准引入的方法
int *p2 = 0; //直接将p2初始化为字面常量0
//需要先#include cstdlib
int *p3 = NULL; //等价于 int *p3 = 0; - nullptr 是一种特殊类型的字面值,可以被转换成任意其他的指针类型
- NULL:预处理变量,在头文件 cstdlib 中定义,值为0,由预处理器负责管理,所有无需添加 std::
- 使用预处理变量时,预处理器会自动地将它替换为实际值,因此,使用NULL初始化指针和使用0初始化指针是一样的,在新标准下,使用 nullptr 是最好的
- 不能将变量直接赋给指针
1
2
3int zero = 0;
int *p = zero; //这是错误的
int *p = &zero; //这是正确的 - 在大多数编译器环境下,如果使用了未经初始化的指针,则该指针所占内存空间的当前内容将被看作一个地址值,访问该指针就相当于去访问一个不存在的位置上不存在的对象。但如果指针所占空间中恰好有内容,而这些内容有被当作了某个地址,那就很难分清是否合法
- 赋值和指针
- 二者都能提供对其他对象的间接访问
- 引用本身并非一个对象,一旦定义了引用,就无法令其再绑定到另外的对象,之后每次使用这个引用都是访问它最初绑定的对象
- 指针则和除引用之外的任何变量一样,给指针赋值就是令它存放一个新的地址,从而指向新的对象
- 如果指针的值是0,条件取 false,任何非0指针对应的条件值均为 true
1
2
3
4
5
6
7int ival = 1024;
int *pi = 0; //pi合法,是一个空指针
int *pi2 = &ival; //pi2是一个合法的指针,存放着ival的地址
if (pi)
// ...
if (pi2) //pi2指向ival,因此它的值不是0,条件的值是true
// ... - 指针相等的情况:
- 它们都是空指针
- 它们都指向同一个对象
- 它们都指向同一个对象的下一地址
- 注意:当一个指针指向某对象,同时另一个指针指向另外对象的下一地址,此时也有可能出现这两个指针相同的情况
- void*指针
- 这是一种特殊的指针类型,可用于存放任意对象的地址,但我们并不能知道地址中到底是什么类型
1
2
3double obj = 3.14, *pd = &obj;
void *pv = &obj; //obj可以是任意类型的对象
pv = pd; //pv可以存放任意类型的指针 - 可以和别的指针比较
- 可以作为函数的输入或输出
- 可以赋给另外一个void*指针
- 不能直接操作其所指的对象,因为我们并不知道其对象的类型
- 这是一种特殊的指针类型,可用于存放任意对象的地址,但我们并不能知道地址中到底是什么类型
- 引用(左值引用)
- 可以将空格写在类型修饰符和变量名中间
1
2int* p; //合法,但容易产生误导
int* p1, p2; //p1是指向int的指针,p2是int - 指向指针的指针
- 通过 * 的个数可以区分指针的级别
1
2
3
4
5
6
7
8
9
10
11
12
13
14
using namespace std;
int ival = 1024;
int *pi = &ival; //pi指向一个int型的数
int **ppi = π //ppi指向一个int型的指针
cout << ival << endl;
cout << pi << " " << *pi << endl;
cout << ppi << " " << **ppi << endl;
/*
1024
0xcda4fffb64 1024
0xcda4fffb58 1024
*/
- 通过 * 的个数可以区分指针的级别
- 指向指针的引用
- 引用本身不是一个对象,所有不能定义指向引用的指针,但指针是对象,所以存在对指针的引用
1
2
3
4
5
6int i = 42;
int *p; //p是一个int型指针
int *&r = p; //r是一个对指针p的引用
r = &i; //r引用了一个指针,因此给r赋值&i就是令p指向i
*r = 0; //解引用r得到i,也就是p指向的对象,将i的值改为0 - 要理解 r 的类型到底是什么,最简单的办法就是从右向左阅读r的定义
- 离变量名最近的符号对变量的类型有最直接的影响(因此 r 是一个引用)
- 声明符的其余部分用以确定 r引用的类型是什么
- 声明的基本数据类型部分指出 r引用的是一个int指针
- 引用本身不是一个对象,所有不能定义指向引用的指针,但指针是对象,所以存在对指针的引用
- const 限定符:通过关键字 const 对变量的类型加以限定(定义为常量),创建时必须初始化,因为之后任何对其进行赋值的行为都将不被允许
- 默认状态下,const 对象仅在文件内有效
- 当多个文件中出现了同名的 const 变量时,其实等同于在不同文件中分别定义了独立的变量
- 如果想在多个文件之间共享 const 对象,必须在变量的定义之前添加 extern 关键字
- 对常量的引用:把引用绑定到 const 对象上,但该引用不能被用作修改它所绑定的对象
1
2
3
4
5const int ci = 1024;
const int &r1 = ci; //正确:引用及其对应的对象都是常量
r1 = 42; //错误:r1是对常量的引用
int &r2 = ci; //错误:试图让一个非常量引用指向一个常量对象 - 临时量对象:所谓临时量对象就是当编译器需要一个空间来暂存表达式的求值结果时临时创建一个未命名的对象
1
2
3
4
5
6
7
8double dval = 3.14;
const int &ri = dval;
//此处编译器会翻译成如下形式
const int temp = dval; //由双精度浮点生成一个临时的整型常量
const int &ri = temp; //让ri绑定这个临时量
//如果ri不是常量,就会运行对ri赋值,就会改变ri所引用的对象的值
//但此时ri绑定的对象是一个临时量而非dval,所以,就算改变ri的值
//也没办法改变dval的值,毫无意义,C++把这种行为归为非法 - 对 const 的引用可能引用一个并非 const 的对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22int i = 42;
int &r1 = i; //引用ri绑定对象i
const int &r2 = i; //r2也绑定对象i,但是不允许通过r2修改i的值
r1 = 0; //正确:r1并非常量,可以修改i的值
r2 = 0; //错误:r2是一个常量引用
using namespace std;
int main()
{
int i = 42;
int &r1 = i;
const int &r2 = i;
cout << r1 << " " << i << " " << r2 << endl;
r1 = 0;
cout << r1 << " " << i << " " << r2 << endl;
return 0;
}
//输出:
42 42 42
0 0 0 - 指向常量的指针不能用于改变其所指对象的值
- 要想存放常量对象的地址,只能使用常量的指针
1
2
3
4const double pi = 3.14; //pi是个常量,它的值不能改变
double *ptr = π //错误:ptr是一个普通指针
const double *cptr = π //正确:cptr可以指向一个双精度常量
*cptr = 42; //错误:不能给*cptr赋值 - 常量指针:
- 指针是对象,所以允许把指针本身定位常量
- 常量指针必须初始化,而且一旦初始化完成,它的值(存放在指针中的地址)就不能再改变了
- 将 *号 放在 const 关键字之前用以说明指针是一个常量,这样书写也隐含了 不变的是指针本身的值而非指向的那个值
1
2
3
4
5
6
7
8
9
10
11
12
13int errNumb = 0;
int *const currErr = &errNumb; //curErr将一直指向errNumb
const double pi = 3.14159;
const double *const pip = π //pip是一个指向常量对象的常量指针
*pip = 2.72; //错误:pip是一个指向常量的指针
//如果curErr所指的对象(也就是errNumb)的值不为0
if (*curErr)
{
errorHandler();
*eurErr = 0; //正确:把curErr所指的对象的值重置
}
- 顶层 const:表示指针本身是个常量,也可以表示任意的对象是常量
- 底层 const:表示指针所指的对象是一个常量,用于声明引用的const都是底层const
- 指针类型既可以是 顶层const 也可以是 底层const
1
2
3
4
5
6int i = 0;
int *const p1 = &i; //不能改变p1的值,这是一个顶层const
const int ci = 42; //不能改变ci的值,这是一个顶层const
const int *p2 = &ci; //允许改变p2的值,这是一个底层const
const int *const p3 = p2; //靠右的const是顶层,靠左的是底层
const int &r = ci; //用于声明引用的const都是底层const - 当执行对象的拷贝操作时,常量时顶层还是底层的区别就很明显,执行拷贝操作并不会改变被拷贝对象的值
1
2
3
4
5
6
7i = ci; //正确:拷贝ci的值,ci是一个顶层const,对此操作无影响
p2 = p3; //正确:p2和p3指向的对象类型相同,p3顶层const的部分不影响
int *p = p3; //错误:p3包含底层const的定义,而p没有
p2 = p3; //正确:p2和p3都是底层const
p2 = &i; //正确:int*能转换成const int*
int &r = ci; //错误:普通的int&不能绑定到int常量上
const int &r2 = i; //正确:const int&可以绑定到一个普通int上 - p3既是顶层const也是底层const,拷贝p3时可以不在乎它是一个顶层const,但是必须清楚它指向的对象得是一个常量
- 常量表达式:指值不会改变并且在编译过程就能得到计算结果的表达式
- C++11新标准规定,允许将变量声明为 constexpr 类型以便由编译器来验证变量的值是否是一个常量表达式,声明为 conxtexpr 的变量一定是一个常量,且必须用常量表达式初始化
1
2constexpr int sz = size(); //只有当size是一个 constexpr 函数时
//才是一条正确的声明语句 - 一个 constexpr 指针的初始值必须是 nullptr 或者 0,或者是存储于某个固定地址中的对象
- 在 constexpr 声明中如果定义了一个指针,则 限定符constexpr 仅对指针有效,与指针所指的对象无关
1
2
3
4
5
6
7
8
9
10const int *p = nullptr; //p是一个指向整型常量的指针
constexpr int *q = nullptr; //q是一个指向整数的常量指针
//constexpr 所定义的对象置为了顶层 const
//constexpr 指针既可以指向常量也可以指向一个非常量
constexpr int *np = nullptr; //np是一个指向整数的常量指针,其值为空
int j = 0;
constexpr int i = 42; //i的类型是整型常量
//i 和 j 都必须定义在函数体之外
constexpr const int *p = &i; //p是常量指针,指向整型常量i
constexpr int *p1 = &j; //p1是常量指针,指向整数j—-
- 类型别名,让复杂的类型名字变得简单和易于理解
- 两种用于定义类型别名的方法:
- 使用关键字 typedef:这是传统方法,关键字 typedef 作为声明语句中的基本数据类型的一部分出现,含有 typedef 的声明语句定义的不再是变量而是类型别名
1
2
3typedef double wages; //wages是double的同义词
typedef wages base, *p; //base是double的同义词,p是double*的同义词
wages hourly, weekly; //等价于 double hourly,weekly; - 新标准规定了一种新方法:使用 别名声明 来定义类型的别名,使用关键字 using 作为别名声明的开始
1
2using SI = Sales_item; //SI是 Sales_item的同义词
SI item; //等价于 Sales_item item;
- 使用关键字 typedef:这是传统方法,关键字 typedef 作为声明语句中的基本数据类型的一部分出现,含有 typedef 的声明语句定义的不再是变量而是类型别名
- auto 类型说明符:C++11新标准引入的,能让编译器替我们去分析表达式所属的类型
- 使用 auto 也能在一条语句中声明多个变量,但因为一条语句只能有一个基本数据类似,所以该语句中所有变量的初始基本数据类型必须一样
1
2auto i = 0, *p = &i; //正确:i是整数、p是整型指针
auto sz = 0, pi = 3.14; //错误:sz和pi的类型不一致 - 编译器会适当地改变结果类型使其更符合初始化规则
- auto 一般会忽略掉 顶层const,同时 底层const 则会保留下来
1
2
3
4
5const int ci = i, &cr = ci;
auto b = ci; //b是一个整数(ci的顶层const特性被忽略)
auto c = cr; //c是一个整数(cr是ci的别名,ci本身是一个顶层const)
auto d = &i; //d是一个整型指针(整数的地址就是指向整数的指针)
auto e = &ci; //e是一个指向整数常量的指针(对常量对象取地址是一种底层const) - decltype 类型指示符:C++11新标准引入的第二种类型说明符,作用是选择并返回操作符的数据类型
- 如果 decltype 使用的表达式是一个变量,则 decltype 返回该变量的类型
1
2
3
4
5
6decltype(f()) sum = x; //sum的类型就是函数f的返回类型
const int ci = 0, &cj = ci;
decltype(ci) x = 0; //x的类型是const int
decltype(cj) y = x; //y的类型是const int&,y绑定到变量x
decltype(cj) z; //错误:z是一个引用,必须初始化 - decltype 的结果可以是引用类型
1
2
3int i = 42, *p = &i, &r = i;
decltype(r + 0) b; //正确:加法的结果是int,因此b是一个(未初始化的)int
decltype(*p) c; //错误:c是int&,必须初始化 - 如果 decltype 使用的是一个不加括号的变量,则得到的结果就是该变量的类型;如果给变量加上一层或多层括号,编译器就会把它当成是一个表达式
1
2
3//decltype 的表达式如果是加上了括号的变量,结果将是引用
decltype((i)) d; //错误:d是int&,必须初始化
decltype(i) e; //正确:e是一个(未初始化的)int - 注意:decltype((variable))(注意是双层括号)的结果永远是引用,而decltype(variable)结果只有当 variable 本身就是一个引用时才是引用
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Aikikoの小窝!