C语言之所以四十年长盛不衰,根本在于它对现代计算机提供了一个底层的高级抽象:凡是比它低的抽象都过于简陋,凡是比他高的抽象都可以用C语言构造出来。C++成功的根本原因,恰恰是因为它试图提供一些高级的抽象机制,但是其根基与C在同一层面。
任何一句C++都有着深刻的C语言背景,可以直接落实为C语言,进而落实为任何一种计算机最底层的机器码。这一点是任何解释型语言都做不到的。另一方面,C++又有着强大的抽象能力,它以奇妙的方式融合着5种编程范式,即面向过程、基于对象、面向对象、泛型和函数式
什么是一门语言的核心部分?就是指一门语言不需要其他任何库(包括标准库)支持的那部分。只要是一个符合标准的C++语言的编译器,无论运行在什么硬件和操作系统上,只要程序员使用的是C++语言,就应该可以使用的那部分语言特性
对象:一块儿能存储数据并具有某种类型的内存空间
while(cin>>val)
当使用iostream对象作为条件时,其效果是检测流的状态。如果遇到EOF或者无效输入,iostream对象会使条件变为假
通常使用.h作为头文件后缀,但也有一些程序员习惯.H .Hpp .hxx后缀,标准库头文件通常不带后缀。编译器一般不关心头文件名的形式,但有的IDE对此有特殊要求
一个C/C++程序仅能包含函数及变量的定义(及声明)。
#include<iostream>
using namespace std;
int a;
a = 1; //错误,不能包含赋值语句
int main()
{
return 0;
}
程序中允许出现空格、制表符或换行的地方,都允许使用注释
第2章 变量和基本类型
数据类型尺寸在不同机器上有所差别,C++只规定了最小尺寸。char的大小应和一个机器字节一样
推荐使用int 、long long 、double
(short范围太小,long一般和int范围一样,float精度不够,速度也慢,double有硬件加速)
32位机器 | 64位机器 | |
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
long | 4 | 8 |
long long | 8 | 8 |
float | 4 | 4 |
double | 8 | 8 |
各类指针(与寻址空间相关) |
4 | 8 |
初始化和赋值是两种不同的操作,虽然二者区别几乎可以忽略不计。初始化的含义是创建变量时赋予其一个初始值,而赋值的含义是把对象的当前值擦除,以一个新值代替
以下4条语句均可以初始化:
int a = 0;
int a(0);
int a = {0};
int a{0};
如果用列表初始化(花括号)初始内置类型变量时,如果存在信息丢失危险,编译器会报错
默认初始化(不是指用默认值初始化,而是包含以下情况)
定义于任何函数体之外的内置类型变量被初始化为0(0.0、false、‘\0’、nullptr)
定义于函数体内部的内置类型变量不被初始化
类变量由默认构造函数初始化
变量声明和定义(多文件共享变量时才有用)
变量声明规定了变量的类型和名字,除此之外,定义还申请存储空间,也可能初始化
extern int j; //声明
int j; //定义
标识符(identifier)
长度没有限制,必须以字母或下划线开头
用户自定义的标识符中不能连续出现两个下划线,也不能以下划线连大写字母开头。此外,定义在函数体外的标识符不能以下划线开头
里层作用域定义的变量会覆盖外层定义的同名变量
引用即别名,并非对象,因此不能定义引用的引用
定义引用时,程序把引用和它的初始值绑定在一起,而不是将初始值拷贝给引用。一旦初始化完成,引用将和它的初始值对象一直绑定在一起。因为无法令引用重新绑定到另一个对象上,因此引用必须初始化。
指针与引用的区别:
(1)指针本身是一个对象,允许对指针赋值和拷贝,而且可以在其生命周期内先后指向不同对象
(2)指针无需在定义时初始化
复杂声明
int *&r = p; //r是一个引用,引用int型指针
int *p[4]; //p是一个4元素数组,每个元素都是int型指针
int (*p)[4]; //p是一个指针,指向一个含有4个int型指针的数组
int &r[4]; //r是一个数组,每元素都是引用。错误,引用不是对象,不存在引用的数组
int (&r)[4] = arr; //r是一个引用,引用一个含有4个int型指针的数组
const
const int i = get_val(); //正确,运行时初始化
const int j = 512; //正确,编译时初始化
const的引用
int i = 5;
const int &j = i; //不能通过j改变i的值,但i的值本身可以改变
const和指针
顶层const:指针本身是个常量,一旦初始化完成,不能再指向其他对象,但可以改变所指对象的值
底层const:可以改变所指向的对象,但不能通过该指针改变所指对象的值
int i = 5;
const int *p1 = &i; //底层const
int *const p2 = &i; //顶层const
const int *const p3 = &i; //既是底层又是顶层
当赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数。无符号数与带符号数运算时,带符号数会被自动转化为无符号数,把负数转化成无符号数,结果等于这个负数加上无符号数的模。比如对于8位的unsigned char,最大能表示256个数,将-1赋给unsigned char结果为255
将赋给带符号数一个超出它表示范围的值时,结果是未定义的(即溢出)
赋给指针的只能是相同数据类型对象的地址,或者是另外一个相同数据类型的指针
int val = 2;
int *p1 = &val;
int *p2 = p1;
空指针
int *p1 = nullptr; //nulllptr是特殊类型的字面值,可以被转化成任意其他的指针类型
int *p2 = 0; // 0代表空指针
int *p3 = NULL; //等价于int *p3 = 0,需包含cstdlib头文件,不推荐
void*是一种特殊的指针类型,可用于存放任意对象的地址。
利用void*指针能做的事比较有限:那它和别的指针比较、作为函数的输入或输出,或者赋给另一个void*指针。不能直接操作void*所指的对象,因为我们并不知道这个对象是什么类型,也就无法确定能在对象上做哪些操作。以void*的视角来看内存空间也就仅仅是内存空间,不能访问内存空间中的对象
auto让编译器通过初始值推算变量类型,因此auto定义的变量必须有初始值
auto i = 0, *p = &i;
因为一条声明语句只能有一个基本数据类型,所以使用auto声明时必须保证类型一致
auto i = 0, *p = &i; //正确,i是int,p是指向int的指针
auto j = 0, pi = 3.14; //错误,j与pi类型不一致
定义数组时必须指定类型,不能用auto通过初始值列表推断
decltype选择并返回操作数的数据类型(编译器只分析表达式类型,并不实际计算表达式的值)
当预处理器看到#include标记时会用指定的头文件的内容代替#include
使用头文件保护符能够有效防止重复包含的发生:
#ifndef CLASSNAME
#define CLASSNAME
/* …… */
#endif
为保证唯一性,通常用头文件中类的名字来构建头文件保护符,并且使用大写
头文件中一般不应该使用using声明
string
string在std命名空间中定义
字符串字面值(也叫字符串常量)不是string的对象(是字符数组)
字符串字面值中会包含最后的’\0’(编译器自动添加)
char s3[] = “value”; //s3最后包含’\0’
char s4[5] = “value”; //错误,s4空间不够,无法容纳最后的’\0’
使用数组下标时,通常将其定义为size_t类型,size_t是一种机器相关的类型,它被设计的足够大以便能够表示内存中任意对象的地址
初始化方式
string s;
cin >> s;
在执行读取操作时,string对象会自动忽略开头的空白字符,从第一个非空白字符开始读起,直到遇到下一处空白为止。
string line;
getline(cin, line);
getline从给定数据流中读取一行(包含其中的空白字符),并存入给定string对象(不包含换行符)
当使用运算符+连接string对象、字符常量、字符串常量时,必须保证+号两侧至少有一个是string对象
string s1 = “hello”;
string s2 = s1 + ‘,’ + “world”; //正确,等价于(s1+’,’)+“world”
string s3 = “hello” + ‘,’ + s1; //错误,“hello”无法直接与‘,’相加
vector
#include<vector>
using namespace std;
初始化(注意区分花括号与圆括号)
vector<int> v1 = {1,2,3}; //列表初始化
vector<int> v2{1,2,3}; //与上面等价
vector<int> v3 = v1; //将v1中的元素拷贝到v3
vector<int> v4(v1); //与上面等价
vector<int> v5(5,1); //用5个1初始化
vector<int> v6(5); //5个元素,每个都初始化为0
如果用花括号,初始化过程会尽可能把花括号内的值当成元素值的列表处理,只有在无法执行列表初始化时才会考虑其他初始化方式
vector<int> v1(10); //10个0
vector<int> v2{10}; //1个10
vector<int> v3(10,1); //10个1
vector<int> v4{10,1}; //10和1
vector<string> v5{“hi”}; //列表初始化
vector<string> v6{10}; //10个空串
vector<string> v7{10, “hi”}; //10个“hi”
若用列表初始化,花括号里值必须与元素类型相同,显然上述int值无法初始化string
vector允许拷贝和赋值,数组不允许
int a[] = {0,1,2};
int b[] = a; //错误
b = a; //错误
标准库类型,如vector、string限定使用的下标必须是无符号类型,而内置数组下标运算可以处理负数
int *p = &a[2];
int j = p[1]; //*(p+1),即a[3]
int i = p[-2]; //*(p-2),即a[0], C++不支持反向索引
第4章 表达式
优先级规定了运算对象的组合方式,但没有规定求值顺序
有4种运算符规定了求值顺序:逻辑或、逻辑与,条件运算符、逗号运算符
左值和右值
当一个对象被用作右值时,用的是对象的值(内容);当对象被用作左值时,用的是对象的身份(地址)
可以把左值当成右值使用(使用他的值),但是不能把右值当成左值使用
条件表达式(expr1?expr2:expr3)中,expr2和expr3只有一个会被计算。若expr2与expr3类型不同,会进行自动类型转换,例如
int n = 1;
cout<< (n>10 ? (double)n : n) / 2;expr2为double,expr3为int,因此expr3会被自动转化为double,与expr1是否满足无关。故输出结果为0.5
除法运算中,如果两个运算对象符号相同,则商为正,否则为负。商一律向0取整(即直接切除小数部分)
逗号运算符按照从左到右顺序依次求值,最后返回的是右侧表达式的值。一般用在for循环中
a += b 与 a = a + b 的唯一的区别是左侧运算对象的求值次数。
使用复合运算符只求一次,普通运算符求值两次
++i与i++的区别:
++i把i的值加1并返回
i++先保存i副本,然后将i加1,返回i副本
sizeof
sizeof返回一条表达式或一个类型名字所占的字节数 (size_t类型)
对指针执行sizeof运算得到指针本身所占空间大小(即与指针类型无关)
int *p1;
double *p2;
cout<<sizeof(p1)<<endl<<sizeof(p2)<<endl;
输出结果均为4
sizeof不会实际计算运算对象的值,因此解引用一个空指针也是安全的
对数组执行sizeof,等价于对数组中所有元素各执行一次sizeof并求和。sizeof不会把数组当成指针处理,因此可以通过以下方式获取数组长度
sizeof(arr)/sizeof(*arr)
sizeof返回结果为常量表达式,可以用于指定数组维度
第5章 语句
一个块就是一个作用域
else与离他最近的尚未匹配的if匹配
switch
case标签必须是整形常量表达式
如果某个case标签匹配成功,将从该标签开始往后顺序执行所有case分支,除非程序显示地中断了这一过程,否则直到switch的结尾处才会停下来
for(init-statemen; condition; expression) { /* */ }
三部分中的任意一部分都可以省略 for(;;) { }
init-statemen部分可以定义多个对象,用逗号分隔,但是所有变量类型必须相同
expression部分也可以有多个,比如for(; ; ++i,++j)
do{
statement
{
while(condition); //最后必须有分号
范围for语句 (C11)
for (declaration : expression)
statement
expression表达的必须是一个序列,比如用花括号括起来的初始值列表、数组、vector或者string。这些类型的共同特点是拥有能返回迭代器的begin和end成员
如果需要对序列中的元素执行写操作,循环变量必须声明成引用类型。
要使用范围for循环处理多维数组,除了最内层的循环,其他所有循环的控制变量都应该是引用类型(为了避免数组被自动转化为指针)
vector<int> v = {0,1,2,3,4,5,6,7,8,9};
for (auto &r : v)
r *= 2;
等价于:
for (auto beg = v.begin(), end = v.end(); beg != end; ++beg) {
auto &r = *beg;
r *= 2;
}
在范围for语句中预存了end()的值,因此不能在范围for中改变序列的长度
标准库类型限定使用的下表必须是无符号类型,而内置下表可以是负数
int *p = &a[2];
int k = p[-2]; //正确,p[-2]仅仅代表*(p-2),即将指针p前移两个int单位,并不是反向索引
auto p = v.begin();
while(p != v.end())
cout<< *p++ <<endl;
//*p++等价于*(p++),即先存储p的副本,将p加1,再返回之前的p作为*的操作对象
第6章 函数
函数是一个命了名的代码块
形参是实参的初始值
编译器能以任意可行的顺序对实参求值
因为数组不允许拷贝,所以无法以值传递的方式使用数组参数,数组会被转化成指向首元素的指针(容器允许拷贝,可以直接作为函数形参,当然也可以使用引用避免拷贝)
//以下三种形式是完全等价的,后两种会被自动转化为第一种
void print(int *);
void print(int []);
void print(int [10]);
由于数组是以指针的形式传递给函数的,所以函数并不能获得数组的尺寸,通常有以下3种方法传递长度信息:
1.使用约定标记,如C风格字符串以 '\0' 作为结束标志
2.使用标注库规范。如 void print(int *begin, int *end)
3.显示传递数组大小
函数返回值
返回 void 的函数允许没有return 语句,因为函数最后一句后面会隐式地执行 return
main函数允许没有 return 语句,此情况编译器会隐式插入一条 return 0
int fun(int a) { if(a >= 0) return a; else if(a < 0) return -a; } /*错误,虽然从逻辑上要么a>=0要么a<0,但是从语法上,存在 if 和 else if 均不成立的情况,此情况未定义返回值,所以报错*/
返回局部对象的引用或指针都是错误的。一旦函数完成局部对象被释放,指针将指向一个不存在的对象
调用一个返回引用的函数得到左值,其他返回类型得到右值
char & get_val(string &str, int id) { return str[id]; //假设id索引有效 } string s = "hello"; get_val(s, 0) = 'H'; //将函数作为左值
因为数组不能被拷贝,所以不能返回数组,但是函数可以返回指向数组的指针或者引用(是指向数组的指针,不是指向数组首元素的指针,需要指定数组长度)
返回数组指针的函数形式如下:
Type (*function(parameter_list)) [dimension]
函数重载:同一作用域内的几个函数名相同但形参列表不同(形参个数不同或者类型不同,与函数返回类型无关)的函数称为重载函数
函数指针
函数类型由返回类型和形参类型共同决定,例如:
bool lengthCompare(const string &, const string &);
该函数类型为 bool(const string &, const string &),要想声明一个指向该函数的指针,只需要将函数名替换为指针即可:
bool (*pf) (const string &, const string &)
当把函数名作为一个值使用时,该函数会自动转化成指针,比如:
pf = lengthCompare;
pf = &lengthCompare; //两式等价,&可选
可以直接使用函数指针调用该函数,无需提前解引用
//等价
bool b1 = pf("hello", "world");
bool b2 = (*pf)("hello", "world");
函数指针应用举例:
int compute(int a, int b, int(*func)(int, int))
{
return func(a, b);
}
int max(int a, int b)
{
return (a > b) ? a : b;
}
int min(int a, int b)
{
return(a < b) ? a : b;
}
int add(int a, int b)
{
return a + b;
}
int main()
{
int m, n, res_max, res_min, res_add;
cin >> m >> n;
res_max = compute(m, n, max);
res_min = compute(m, n, &min);
res_add = compute(m, n, add);
return 0;
}
第7章 类
使用 class 和 struct 定义类的唯一区别就是默认的访问权限
成员函数通过一个名为 this 的隐式参数来访问调用它的那个对象。通过请求该函数的对象地址初始化 this,因此 this 是一个指向当前对象的常量指针
a.fun() 可以理解为 A::fun(&a),其中 a 是 A类创建的对象,fun是A类的成员函数
成员函数声明必须在类内,定义可内可外。定义在类内部的函数时隐式的 inline 函数
构造函数
构造函数的名字必须和类名相同,不能有返回类型
默认构造函数:没有任何参数的构造函数
如果我们没有显式定义构造函数,那么编译器会隐式定义一个合成的默认构造函数,一旦我们定义了其他构造函数,那么除非我们再定义一个默认构造函数,否则类将没有默认构造函数
通过友元机制可以允许其他类或者函数访问类的非公有成员
友元声明仅仅指定了访问权限,而非普通意义上的函数声明。必须再专门对函数进行一次声明
类的静态成员
类的静态成员是类的属性,存在于任何对象之外。类似的,静态成员函数也不与任何对象绑定,没有 this 指针。但是和类的其他成员一样,仍然可以使用类的对象、引用或指针访问静态成员。也可以使用作用域运算符通过类名直接访问
第9章 顺序容器
一个容器就是一些特定类型对象的集合
vector | 最常用 |
string | (严格来说不算容器) |
deque | 双端队列 |
list | 双向链表 |
forward_list | 单向链表 |
array | 固定大小数组 |
forward_list 设计的目的是为达到与最好的手写的单向链表相当的性能。因此 forward_list 没有 size 操作,因为保存或计算其大小就会比手写链表多出额外开销。对其他容器而言,size保证是一个快速的常量时间操作。
一般来说每个容器都定义在一个头文件中,文件名与类型名相同。容器均定义为模板类
begin和end成员函数
end并非指向尾元素,而是指向尾元素之后的位置。迭代器范围为:[begin, end)
通过for循环遍历容器时,使用 != 作为判断条件的原因:所有标准库容器的迭代器都定义了 == 和 !=,但多数没有定义 < 运算
begin和end有多个版本:带 r 的返回反向迭代器; c 开头的返回 const 迭代器
list<string> a = {"hello", "world", "!"};
auto it1 = a.begin(); // list<string>::iterator
auto it2 = a.rbegin(); // list<string>::reverse_iterator
auto it3 = a.cbegin(); // list<string>::const_iterator
auto it4 = a.crbegin(); // list<string>::const_reverse_iterator
其中不以c开头的函数都是重载过的,对非常量对象调用时,得到的是 iterator 版本,只有对const对象调用时才得到const版本,普通 iterator 可以转换为对应 const_iterator,反之不行
容器初始化
除了常规的默认初始化,拷贝初始化,列表初始化方式之外,还可以(array除外)拷贝由一对迭代器指定的元素范围
将一个容器初始化为另一个容器的拷贝时,必须要求两个容器的容器类型和元素类型均匹配
通过传递迭代器拷贝一个范围时,不要求容器类型一致,甚至不要求元素类型一致,只要拷贝的元素能转化为新元素类型即可
list<string> a = {"hello", "world", "!"};
vector<const char*> b = {"HELLO", "WORLD", "!!"};
list<string> c(a); //正确,都是list,string
deque<string> d(a); //错误,容器类型不匹配,一个list,一个deque
vector<string> e(b); //错误,元素类型不匹配,一个string,一个const char*
forward_list<string> f(b.begin() + 1, b.end()); //正确,const char*能够转化为string
顺序容器(array除外)还额外提供一个可以指定容器大小(以及一个初始值)的构造函数
vector<int> v1(10); // 10个0
list<int> v2(10, 1); // 10个1
容器适配器
适配器是STL中一个通用概念,容器、迭代器、函数都有适配器。
本质上,适配器是使某种事物的行为看起来像另外一种事物的机制
STL中定义了三个顺序容器适配器:stack、queue、priority_queue。
默认情况下,stack 和 queue 是基于 deque 实现的,priority_queue 是基于 vector 实现的。我们只能使用适配器操作,不能使用底层容器类型的操作。
A a; | 只有这两种,不能列表初始化 |
A a(c); | |
a.empty() | |
a.size() | |
关系运算 | ==、!=、<、<=、>、>= |
只能通过适配器各自的成员函数访问其首位元素,不能访问中间元素,也不支持迭代器遍历
stack
#include<stack>
stack<int> s;
s.push(3);
s.pop(); //删除栈顶元素,但不返回
int t = s.top(); //返回栈顶元素,但不删除
queue
#include<queue>
queue<int> q;
q.push(3);
q.pop(); //删除队首元素,但不返回
int front = q.front(); //返回队首元素,但不删除
int back = q.back(); //返回队尾元素,但不删除
priority_queue
在queue头文件中定义
用法和stack类似,只有push,pop,top(返回队首元素,即优先级最高的元素)操作
第12章 动态内存
C++通过new与delete进行动态内存管理
new为对象分配空间并返回指向该对象的指针
delete接受一个指向动态对象的指针,销毁该对象并释放关联内存
使用new和delete管理内存容易出现以下三个问题:
1. 忘记delete内存
2. 使用已经释放的对象
3. 同一块内存释放两次(两个指针指向相同动态分配对象时)
为避免这些问题,C++11标准库通过智能指针类型管理动态对象智能指针的行为类似常规指针,重要区别在于它负责自动释放所指对象。新标准提供的两种智能指针区别在于管理底层指针的方式。
shared_ptr 允许多个指针指向同一对象
unique_ptr 独占所指的对象
此外还定义了伴随类weak_ptr,它是一种弱引用,指向shared_ptr 所管理的对象。
智能指针也是模版,三种类型都定义在memory头文件中
int *p = new int[get_size()];
方括号中的大小必须为整型,但不必是常量。也可以是0,此时new返回一个合法的非空指针