C++ 入门(上)
基于 _C++ Primer Fifth _ 编写
不愧是 C++,这才只是入门,只到 C++ 11
开始 Getting Started
万恶之源:
|
文件的后缀名有很多,最常见的应该是 .cc
和 .cpp
编译器也有很多,如使用 g++
编译:
|
- 添加支持 c++ 11 标准:
-std=c++11
-Wall
生成更多警告信息
输入输出:
- 这里使用了
iostream
库中的标准输入流和标准输出流,该库中还有标准错误流cerr
和clog
<<
是输出操作符,>>
是输入操作符endl
是一种控制符 manipulator,有结束当前行和刷新缓冲区的作用
std::
表示使用命名空间 namespace 的函数等,用于区分
一个读入的例子:
|
>>
左边是一个 istream
,右边是一个对象,并返回左边的操作数为结果
注释方面依旧是两种://
和 /* */
控制流:while
、for
读入未知数量的输入:while(std::cin >> value)
,即当输入流读到 EOF 或无效输入时,会无效,导致条件为 false
键盘中的 EOF 是输入 Ctrl + z
if
语句
C++ 支持类
变量与基础类型 Variables and Basic Types
初级内置类型
算术类型:整数和浮点数
对于 unicode 字符,有 char16_t
和 char32_t
选择使用的类型的原则:
- 如果非负,使用
unsigned
- 整数运算一般使用
int
,如果不够,考虑long long
char
和bool
用于表示特殊的东西- 浮点数运算用
double
注意 signed 和 unsigned 参与运算时会发生隐式转换
字面值:后面可以加 u
、l
、ll
等表示整数,f
、l
表示浮点数,前面加 u
、U
表示 16 位、32 位 Unicode,u8
表示 utf-8 字符串
特殊的 e
编译器自动对每个字符串字面值末尾加 \0
可以用转义字符 excape sequence 表示一些不可打印的字符,如 \n
;也有泛化的表示,如 \40
表示空格
变量
定义时初始化:int i = 0;
,注意这里的 =
和赋值无关,而是表示初始化 initialization
还有另外的初始化方法——列表初始化 list initialization:int i = {0};
或 int i{0}
如果没有初始化值,则是默认初始化
函数内的变量未初始化
为了支持分开编译,声明 declaration 使名字被程序直到,定义 definition 则创建了关联的实体,例如:
|
标识符,作用域
复合类型
复合类型 compound type 是以另一种类型定义的类型,如引用和指针
引用 reference 相当于对象的别名,如
|
引用定义时绑定到一个对象上,且不可更改
指针 pointer 指向另一个类型 int *ip1;
指针保持了另一个对象的地址,通过取址运算符 &
获取一个对象的地址:int *p = &ival;
注意指针类型和指向的对象类型必须匹配
使用解引用运算符 *
来访问对象:
|
注意 *
和 &
在表达式和声明时表达的含义不同
空指针 null pointer 没有指向任何对象:int *p1 = nullptr;
void*
指针可以存任何对象的地址
当然,还有指向指针的指针:int **ppi = π
const 标识符
const
定义常量,必须在定义时初始化,然后不可变
变量可以赋值给常量,常量也可以赋值给变量,因为复制对象并不会改变对象
const int &ri = i;
表示对常量的引用,其不可用于改变值,注意 i
可以是非 const 的
类似的,const int *cpi = &i;
表示对常量的指针,不可用该指针改变其指向的变量的值
const 指针表示其不可改变指向,类似于引用:int *const curErr = &errNumb
总的来说,指向 const 对象的叫低级 const,该对象本身就是 const 的叫顶级 const
- 低级 const 只会出现在复合类型中
- 当复制对象时,高级 const 被忽略
常量表达式 constant expression 表示可以在编译时确定的表达式,如 constexpr int limit = mf + 1;
注意,constexpr
指针暴露的顶级 const:constexpr int *q = nullptr;
处理类型
使用 typedef
给类型起别名:typedef double wages;
还有另一种方法:using wages = double;
auto
自动推断类型
同样的,对于复合类型,忽略顶级 const,如有需要,必须手动加上:const auto f = &i;
decltype
返回操作数的类型,如 decltype(f()) sum = x;
,注意这里编译器并不会调用函数 f()
,而是依据其返回值确定类型
注意,decltype((variable))
始终是一个引用类型
自定义数据结构
定义一个可以直接访问数据元素的“类”:
|
这里有数据成员
为了防止被多处使用的头文件被反复包含而多次编译,使用预处理器 preprocessor 来定义 header 保护,如:
|
字符串,向量和数组 Strings, Vectors, and Arrays
命名空间 using
声明
为了防止每次都要写 std::cin
,可以使用 using 声明:using std::cin;
库 string
类型
string
初始化除了之前说过的,还有
|
注意末尾的 \0
不会被复制到字符串中
字符串的操作:<<
、>>
、getline(is, s)
、s.empty()
、s1 + s2
、s1 = s2
、s1 == s2
、s1 < s2
等
字符串可以直接连接,但是为了兼容 C,字符串字面值不能直接连接
在 cctype
头文件中有很多处理字符的函数,如 islower(c)
和 tolower(c)
等
c
开头的库表示是 C++ 版本的 C 语言库
一种 for 循环的写法:for (auto c : str)
[]
用于索引,范围是 0 <= index < s.size()
库 vector
类型
vector
是一个模板 template,vector<int>
等才是类型
初始化同理
添加元素:push_back()
.size()
返回的是 vector<int::size_type
介绍 Iterator
除了下标以外,还有一种访问方法——迭代器 iterator
.begin()
返回指向第一个元素的迭代器,.end()
返回指向最后一个元素的下一位的迭代器
迭代器可以解引用,自增或自减,判断是否相等
注意因为 .end()
返回的迭代器不指向某个元素,所以不可以自增或自减
一个使用迭代器迭代的写法:for (auto it = s.begin(); it != s.end(); ++it)
类似指针,vector<int>::const_iterator it;
只可读,不可写
如果操作的对象是 const 的,则 .begin()
等返回的是 const_iterator
,如
|
为了能够显式指明想要的是 const_iterator
,可以使用 .cbegin()
和 .cend()
string 和 vector 的迭代器同样支持加减、大小比较,这种运算叫做迭代器算术 iterator arithmetic
特别的,两个迭代器相减的返回值的类型是 difference_type
数组
数组相当于固定大小的 vector
对数组取下标的变量的类型是 size_t
,其定义在 cstddef
中
数组和指针紧密相连
数组有类似于容器的两个迭代器函数:int *pbeg = begin(arr), *pend = end(arr);
两个指针相减的返回值的类型是 ptrdiff_t
对于 C++ 来说,最好不要使用 C 风格的 string,故这里不作介绍
多维数组
多维数组也可以使用 for 循环,但是除了最内层以外都要使用引用:
|
表达式 Expressions
基础
单元、二元、三元运算符
运算符重载
左值与右值
优先级与结合性
表达式中的计算是没有顺序的,如 int i = f1() * f2();
是无法得知哪个函数先调用的;cout << i << ++i << endl;
也是一个未定义的行为
有定义计算循序的运算符只有 &&
、||
、? :
、,
算术运算符
在计算中,小的整数类型被提升为较大的整数类型
逻辑与关系运算符
赋值运算符
返回左操作数,优先级低
自增/自减
一种常见的写法:*iter++
成员访问函数
->
返回的是一个 lvalue;如果对象是 lvalue,则 .
返回的是一个 lvalue,,反之则是 rvalue
位运算
由于在位运算中符号位如何处理是未知的,所以推荐只对 unsigned 变量进行位运算
sizeof(类型)
返回 size_t
,还有一种用法:sizeof 表达式
,注意表达式不会被求值
类型转换
隐式类型转换
显式类型转换:
static_cast
const_cast
改变了低级的 const,在重载函数中有用reinterpret_cast
基于操作数的 bit 的低级重新解释
总之,要尽可能避免类型转换
语句 Statements
空语句应该有注释,以免被人忽略
块与块作用域
控制语句
对应 switch
,最好一定要定义一个 default
,即使是空的
定义在 while
里的变量在每次迭代时都会创建和销毁
在 stdexcept
中定义了通用的异常类:exception
、runtime_error
、range_error
、overflow_error
、underflow_error
、logic_erro
、domain_error
、invalid_argument
、length_error
、out_of_range
函数 Functions
参数传递
值参数传递 vs 引用参数传递
对于较大的对象,按值传递复制的开销较大,但又不想要改变该对象,可以使用 const
引用
因为在赋值时顶层 const 被忽略,所以 void fcn(const int i)
和 void fcn(int i)
是冲突的
可以传递对数组的引用:f(int (&arr)[10])
二维数组的传递:
|
命令行参数传递:
|
其中 argv[0]
为程序名称
initializer_list
类似于数组,是一个模板类型,可以用来接收未知数量的参数:
|
返回类型与返回语句
一种简单的返回 vector
的方法:
|
如果 main()
没有返回值,则隐式返回 0,在 cstdlib
中预处理的变量表示成功和失败:EXIT_FAILURE
和 EXIT_SUCCESS
对于一个返回一个数组的函数,可以使用尾随返回类型 trailing return type,如
|
特殊用途的特色
inline
可以让编译器将函数展开,但编译器可以拒绝这么做
constexpr
函数隐式 inline
了,同时返回值不一定是 const
以上两者一般定义在头文件中
assert(expr);
:若 expr
为假,终止程序,用于检查不会发生的条件
在编译时添加参数 -D NDEBUG
来关闭 debug 模式
结合一些宏,可以提高调试效率:
|
__func__
输出函数名称__FILE__
文件名__LINE__
当前行数
函数指针
函数指针类似于函数原型:
|
使用起来类似于一个函数别名:
|
可以将其指向其他的函数,但必须函数原型一致
函数指针也可以作为参数,可以像函数一样写,也可以显式写成函数指针的形式:
|
传递参数时直接使用函数名:
|
当然,也可以返回一个函数指针:
|
类 Classes
在类内部定义的成员函数隐式 inline
;如果在外部定义,则要指定是哪个类:Sales_data::print(const string& s) {...}
const
成员函数实际上相当于:const Sales_data *const this
,即指定了 this
的 const 性质——不改变该对象
如果没有构造函数,编译器会自动生成一个默认构造函数,也可以显式指定:Sales_data() = default;
。当然,这只针对内置的类型。
构造函数初始化列表:Sales_data(const string &s): bookNo(s) { }
一个类可以通过友元 friend 使类或函数访问到其私有成员
友元声明只指定了访问,还需要另外声明友元声明的函数,该函数通常和类声明在同一个头文件中
我们可以在类中定义一个类型成员:
|
const 成员函数返回的 *this
是一个 const 对象
每个类实际上定义了自己的新作用域,因此,对于定义在类外部的成员,要使用类名称指定:
|
注意区分初始化 ConstRef::ConstRef(int ii): i(ii) { }
和赋值 ConstRef::ConstRef(int ii) { i = ii; }
其中初始化顺序一定是被声明的顺序,而不是定义中写的顺序,故最好两者顺序一致
指派 delegating 构造函数使用了该类的另一个构造函数执行初始化,如:
|
Sales_data obj();
是一个函数声明,Sales_data obj2;
才是默认构造对象
单个参数的构造函数可以隐式调用构造函数转换,如:
|
为了防止这种转换,可以禁用隐式转换:
|
这样子做了之后也不可用于复制形式的初始化:
|
集合体 aggregate 类
- 所有的数据成员都是 public
- 没有定义任何构造函数
- 没有类内部的初始化
- 没有基类或虚函数
静态成员:
|
静态 static const 成员可以在类内部初始化
IO 库 The IO Library
IO 类
IO 库的头文件:iostream
、fstream
、sstream
为了支持操作 wchar_t
类型的数据,定义了 wistream
、wostream
等类型,对应的有 wcin
、wcout
等
IO 对象不可复制或赋值
stream 有四种情况:eof
、fail
、bad
、good
.rdstate()
返回该 stream 的 iostate
.clear()
有两种重载:
- 无参数的:清空所有情况
- 有一个参数的:
cin.clear(cin.rdstate, ~cin.failbit & ~cin.badbit);
io 的 buffer 刷新的情况:
- 程序正常结束
- buffer 满了
- 显式使用如
endl
、flush
的操作符 - 使用
unitbuf
操作符,使得每次输出操作都会刷新 buffer - 输出流被绑定到另一个流,如
cin
被绑定到cout
,故读cin
会刷新cout
的 buffer- 解除绑定的方法:
cin.tie(nullptr);
- 解除绑定的方法:
文件输入和输出
创建一个 fstream:ifstream input("input.txt");
文件模式:
-
in
、out
-
app
每次写之后 seek 到结尾 -
ate
在打开后 seek 到结尾 -
trunc
截断文件 -
binary
以二进制模式执行 IO 操作
保持文件内容的输出:ofstream app("file2", ofstream::out | ofstream::app)
string
流
当我们想要对一整行的独立字做一些工作时,istringstream
很有用
顺序容器 Sequential Containers
顺序容器类型:vector
、deque
、list
、forward_list
、array
、string
rbegin()
、rend()
.assign()
支持比较大小
当 push
或 insert
一个成员时,我们传递的对象类型的容器的元素类型,且会被复制到容器中;但使用 emplace
等直接在容器管理的空间中使用构造函数来创建元素,如 c.emplace_back("999", 25, 15.99);
一个使用循环来插入元素的方法:
|
如果我们已经不再需要多余的空间了,可以使用 shrink_to_fit()
来返回未利用的内存,但实际实现可以忽略该请求
string 的搜索函数返回的是无符号的 string::size_type
,如果没找到,返回的 string::npos
是 -1,即最大的 string 大小
将字符串转化为数字:stoi(s)
、stod(s)
容器适配器 container adaptor
泛型算法 Generic Algorithms
初见算法
算法不会执行容器操作,而是基于迭代器操作,故算法从来不会改变底层容器的大小
算法通常接受由首尾迭代器指明的范围
只读算法:find
、accumulate
,如 string sum = accumulate(v.cbegin(), v.cend(), string(""));
有的算法可以接受两个序列,如 equal(roster1.cbegin(), roster1.cend(), roster2.cbegin())
注意第二个序列只接受了一个迭代器,我们需要确保第二个序列长度不小于第一个序列
写容器元素的算法:fill
back_inserter
接受对容器的引用并返回一个插入迭代器,当我们通过这个迭代器赋值时,会自动调用 push_back
不少算法都有 _copy
的版本,如 replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);
重新排列的算法,如去重的算法:
|
自定义操作
lambda 表达式代表可调用的代码单元,可以被视为匿名、内联函数,有如下形式:[捕获列表] (参数列表) -> 返回值类型 { 函数体 }
尽管 lambda 出现在函数的内部,其只有在捕获列表中指明了变量,才能使用函数内部的局部变量
for_each()
函数接受一个可调用对象并调用输入范围内的每个对象
当我们定义了一个 lambda 时,编译器生成了一个新的匿名的类。
当然,可以捕捉值,也可以捕捉引用
默认情况下,lambda 不能改变按值捕获的变量的值,但可以加上 mutable
关键字,使得可以改变这个值:
|
fucntional
头中的 bind
可以接受可调用函数和参数,并将参数应用于该函数,形式为:auto 新可调用对象 = bind(可调用对象, 参数列表)
例如:
|
注意 bind
是将参数复制过去,对于不能复制的,可以使用 ref
和 cref
引用:
|
重探迭代器
迭代器的类型:insert
、stream
、reverse
、move
insert
迭代器:
back_inserter
front_inserter
inserter
iostream
迭代器:
istream_iterator<T>in(is)
从输入流is
中读数据istream_iterator<T>end
istream_iterator
的尾迭代器
输出迭代器同理:
|
可以通过提供反向迭代器降序排序:sort(vec.rbegin(), vec.rend());
五类迭代器
算法的结构
算法的几种结构:
alg(beg, end, 其他参数)
alg(beg, end, dest, 其他参数)
alg(beg, end, beg2, 其他参数)
alg(beg, end, beg2, end2, 其他参数)
注意带 _if
和 _copy
后缀的算法
优先使用成员函数版本的算法