第1章 开始
1.1 编写一个简单的C++程序
int main ()
{
return 0;
}
每个C++程序都必须有一个main函数, main函数的返回值类型必须为int型。函数的定义包含四部分:返回类型、函数名、括号包围的形参列表以及函数体。注意,大多数C++语句都要以分号结束。
1.2 初识输入输出
① iostream库:包含istream和ostream,分别表示输入流和输出流。
② 标准输入输出对象:标准输入(cin)、标准输出(cout)、输出警告和错误信息(cerr)、输出程序运行一般性信息(clog)。
#include <iostream> //一般系统头文件用尖括号表示,用户自己编写的则用双引号。一般将所有的#include指令都放在源文件开头位置。
int main()
{
std::cout << "Enter two numbers: " << std::endl; //标准输出,把右边的数据给cout对象,用 << 符号,endl表示结束当前行,并将与设备关联的缓冲区的内容刷到设备中。如果没有std::endl,并且在后面加一个while死循环,会发现屏幕一直不显示。
int v1= 0, v2 = 0;
std::cin >> v1 >> v2; //标准输入,把左边输入的数据给后面的变量,所以用 >> 符号。
std::cout << "The sum of " << v1 << " and " << v2
<< " is " << v1 + v2 << std::endl;
return 0;
}
1.3 注释简介
① 单行注释:以双斜线 // 开始,后面加一个空格,以换行符结束,常用于半行和单行注释。
② 界定符注释:以 /* 开始,以 */ 结束,通常用于多行注释。为了美观和规范,可以每行以星号空格开始写注释,从第二行开始书写。注意:界定符注释不能嵌套使用。
/*
* 功能:注释示例
* 作者:今天也要加油鸭!
* 版本号:v1.0
* 最后更新时间:2021/12/14
*/
int main ()
{
return 0;
}
1.4 控制流
① while语句:
while (condition)
statement
② for语句:
for(int val = 1; val <= 10; ++val)
sum += val;
③ 读取数量不定的输入数据:
while (std::cin >> value) //遇到文件结束符,或遇到一个无效的输入,条件变为假,终止输入,Windows中文件结束符为:Ctrl+Z。
sum += value;
④ if-else语句:
if (condition){
statement;
} else {
}
1.5 类简介
① 点运算符(.):只能用于类类型的对象,其左侧运算对象必须是一个类型的对象,右侧运算对象必须是该类型的一个成员名,运算结果为右侧运算对象指定的成员。
② 调用运算符(()):用来调用一个函数,里面放置实参列表。
1.6 书店程序
//Sales_item.h
#ifndef SALESITEM_H
// we're here only if SALESITEM_H has not yet been defined
#define SALESITEM_H
// Definition of Sales_item class and related functions goes here
#include <iostream>
#include <string>
class Sales_item {
// these declarations are explained section 7.2.1, p. 270
// and in chapter 14, pages 557, 558, 561
friend std::istream& operator>>(std::istream&, Sales_item&);
friend std::ostream& operator<<(std::ostream&, const Sales_item&);
friend bool operator<(const Sales_item&, const Sales_item&);
friend bool
operator==(const Sales_item&, const Sales_item&);
public:
// constructors are explained in section 7.1.4, pages 262 - 265
// default constructor needed to initialize members of built-in type
Sales_item(): units_sold(0), revenue(0.0) { }
Sales_item(const std::string &book):
bookNo(book), units_sold(0), revenue(0.0) { }
Sales_item(std::istream &is) { is >> *this; }
public:
// operations on Sales_item objects
// member binary operator: left-hand operand bound to implicit this pointer
Sales_item& operator+=(const Sales_item&);
// operations on Sales_item objects
std::string isbn() const { return bookNo; }
double avg_price() const;
// private members as before
private:
std::string bookNo; // implicitly initialized to the empty string
unsigned units_sold;
double revenue;
};
// used in chapter 10
inline
bool compareIsbn(const Sales_item &lhs, const Sales_item &rhs)
{ return lhs.isbn() == rhs.isbn(); }
// nonmember binary operator: must declare a parameter for each operand
Sales_item operator+(const Sales_item&, const Sales_item&);
inline bool
operator==(const Sales_item &lhs, const Sales_item &rhs)
{
// must be made a friend of Sales_item
return lhs.units_sold == rhs.units_sold &&
lhs.revenue == rhs.revenue &&
lhs.isbn() == rhs.isbn();
}
inline bool
operator!=(const Sales_item &lhs, const Sales_item &rhs)
{
return !(lhs == rhs); // != defined in terms of operator==
}
// assumes that both objects refer to the same ISBN
Sales_item& Sales_item::operator+=(const Sales_item& rhs)
{
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
// assumes that both objects refer to the same ISBN
Sales_item
operator+(const Sales_item& lhs, const Sales_item& rhs)
{
Sales_item ret(lhs); // copy (|lhs|) into a local object that we'll return
ret += rhs; // add in the contents of (|rhs|)
return ret; // return (|ret|) by value
}
std::istream&
operator>>(std::istream& in, Sales_item& s)
{
double price;
in >> s.bookNo >> s.units_sold >> price;
// check that the inputs succeeded
if (in)
s.revenue = s.units_sold * price;
else
s = Sales_item(); // input failed: reset object to default state
return in;
}
std::ostream&
operator<<(std::ostream& out, const Sales_item& s)
{
out << s.isbn() << " " << s.units_sold << " "
<< s.revenue << " " << s.avg_price();
return out;
}
double Sales_item::avg_price() const
{
if (units_sold)
return revenue/units_sold;
else
return 0;
}
#endif
#include <iostream> //标准库头文件
#include "Sales_item.h" //自定义的头文件
int main()
{
Sales_item total;
if (std::cin >> total) {
Sales_item trans;
while (std::cin >> trans) {
if (total.isbn() == trans.isbn())
total += trans;
else {
std::cout << total << std::endl;
total = trans;
}
}
std::cout << total << std::endl;
} else {
std::cerr << "No data?!" << std::endl;
return -1;
}
return 0;
}
1.7 其他
① 在C++中,= 表示赋值,==表示相等。
② 除非特别需要,尽量使用 ++i 而不是 i++。
③ C++ Primer 英文版官方勘误:C++ Primer errata
参考教程:C++概述
第 2 章 变量和基本类型
2.1 基本内置类型
类型 | 含义 | 最小尺寸 |
---|---|---|
bool | 布尔类型 | 未定义 |
char | 字符 | 8位 |
int | 整形 | 16位 |
long long | 长整型 | 64位 |
float | 单精度浮点数 | 6位有效数字 |
double | 双精度浮点数 | 10位有效数字 |
建议:①当明确知晓数值不可能为负,建议选用无符号类型。
②整数运算用 int,超过 int 表示范围时用 long long。
③算术表达式中,非必要不使用 char 和 bool。
④浮点数运算用 double 即可。
Ⅰ)自动类型转换
1)将一种类型的数据赋值给另外一种类型的变量时就会发生自动类型转换。如:
float f = 10; // 10是int类型,先转换为float类型10.000000,再赋给f
int m = f; // f是float类型,先转换为int类型10,再赋给m
int n = 3.14; // 3.14是float类型,先转换为int类型3,再赋给n
2)在不同类型的混合运算中,编译器也会自动地进行数据类型转换,将参与运算的所有数据先转换为同一种类型,然后再进行计算。转换规则如图:
注:如上图所示,当一个算术表达式中既有无符号数又有 int 值时,那么 int 值会转换成无符号数。因此,切勿混用带符号类型和无符号类型。
unsigned u = 10;
int i = -42;
std::cout << i+i << std::endl; // 输出-84
std::cout << u+i << std::endl; // 如果int占32位,输出4294967264
Ⅱ) 强制类型转换
格式:(type_name)expression 例如:
(float) i;
(int) (a+b);
(float) 10;
(double) x/y; // 先将 x 转换为 double 类型,再除以 y。
(double) (x/y); // 先计算 x/y,再转换为 double 类型。对于除法运算,若 x 与 y 都是整数,则运算结果也是整数,小数部分将被直接丢弃;若 x 与 y 有一个是小数,则运算结果也是小数。
注意:自动类型转换和强制类型转换,都只是本次运算临时性的,不会改变数据本来的类型或者值。
Ⅲ) 字面值常量
主要包括:整型和浮点型、字符和字符串、转义序列、布尔和指针等字面值常量,同时,还可以指定字面值类型。
字符和字符串字面值 | |||
前缀 | 含义 | 类型 | |
u | Unicode 16字符 | char16_t | |
U | Unicode 32字符 | char32_t | |
L | 宽字符 | wchar_t | |
u8 | UTF-8 | char | |
整型字面值 | 浮点型字面值 | ||
后缀 | 最小匹配类型 | 后缀 | 类型 |
u or U | unsigned | f或F | float |
l or L | long | l或L | long double |
ll or LL | long long |
2.2 变量
对于C++程序员,“变量”和“对象”可以互换使用。
Ⅰ) 变量定义基本形式: 类型说明符 变量名1,变量名2,...;
在C++语言中,初始化和复赋值是两个完全不同的概念。注意初始化和赋值的区别,常见的初始化形式有:
int a = 0;
int a = {0};
int a{0};
int a(0);
其中,通过花括号进行初始化的形式被称为列表初始化。 如果使用列表初始化且初始值存在丢失信息的危险,编译器将报错。
#include <iostream>
void function(){
int i;
std::cout << "普通函数内未初始化的值:" << i << std::endl;
}
int k;
int main(int argc, char const *argv[])
{
function();
int j;
std::cout << "main函数内未初始化的值:" << j << std::endl;
std::cout << "全局未初始化的值:" << k << std::endl;
return 0;
}
对于未初始化的变量,输出如下:
普通函数内未初始化的值:22007
main函数内未初始化的值:1525802986
全局未初始化的值:0
Ⅱ)变量的声明和定义:
声明规定了变量的类型和名字,使得名字为程序所知。
定义负责创建与名字相关的实体,申请内存空间,也可能为变量赋一个值。
变量只能被定义一次,但是可以被多次声明。在C++项目开发中,声明一般统一放在头文件里面,在源程序中定义,并且建议在第一次使用变量时再定义。
extern int i; // 想要声明 i,而非定义 i
int j; // 定义 j
extern double pi = 3.1416; // 因为有了显式初始化,extern 的作用被抵消,这里变成定义了,注意:只能作为全局变量的定义,即放在 main 函数外面。
2.3 复合类型
Ⅰ)引用:就是为对象起了另外一个名字(引用即别名),定义引用时就把引用和它的初始值绑定了,并且不能重新绑定,因此引用必须初始化,且初始值必须是一个对象而不是字面值。
int a;
int &b = a; // b其实只是a的一个别名,注意b不是对象。
int &c; // 报错!引用必须初始化
Ⅱ)指针:
int a = 1;
int *p = &a;
int *p1 = nullptr; // 空指针
void *p2 = &a; // p2可以用来存放任意对象的地址,这里存放int类型的 a 的地址
std::cout << *p;
Ⅲ) 指针和定义的区别:指针本身是一个对象,允许赋值和拷贝,可以先后指向不同的对象,并且定义时不是必须初始化(不过建议初始化为 nullptr 或者 0)。
Ⅳ)指向指针的引用:从右往左阅读,离变量名最近的符号对变量的类型有最直接的影响,所以上面的r是一个引用,*说明引用的是一个指针。因此,r是指向int类型指针的引用。
int i = 42;
int *p; // p是int型指针
int *&r = p; // r是对指针p的引用
r = &i;
*r = 0;
2.4 const 限定符
const 限定符用来对变量的类型加以限定,如:const int bufSize = 512。
const int i = get_size();
const int j = 42;
const int k; // 错误,未初始化
int i = 42;
const int ci = i;
int j = ci;
默认状态下,const 对象仅在文件内有效。 要想只在一个文件中定义const,而在其他多个文件中声明并使用它,可以对于 const 变量不管是声明还是定义都添加 extern 关键字。
// file_1.cc 定义并初始化了一个常量,该常量能被其他文件访问
extern const int bufSize = fcn();
// file_1.h 头文件
extern const int bufSize;
Ⅰ)const 的引用:可以把引用绑定在 const 对象上,就像绑定在其他对象上一样,我们称之为对常量的引用。
const int ci = 1024;
const int &r1 = ci; // 正确,引用和对应的对象都是常量
ri = 42; // 错误! r1是对常量的引用,不能修改值
int &r2 = ci; // 错误! 试图让一个非常量引用指向一个常量对象,如果合法,则可以通过改变r2来改变ci,显然不正确
int i = 42;
const int &r1 = i; // 正确
const int &r2 = 42; // 正确
const int &r3 = r1 * 2; // 正确
int &r4 = r1 * 2; // 错误! 如果合法,则可以通过r4修改r1*2的值
Ⅱ)指针和const:
const double pi = 3.14; // pi是个常量,值不能改变
double *ptr = π // 错误! ptr是个普通指针,可能会改变pi
const double *cptr = π // 正确
*cptr = 42; // 错误! cptr不能赋值
double dval = 3.14;
cptr = &dval; // 正确,但是不能通过cptr改变dval的值
指向常量的指针和引用,不过是它们自以为指向了常量,因此会自觉地不去改变所指对象的值,但实际上它们所指对象的值可以通过其他方式改变。
Ⅲ)const 指针:由于指针也是对象,因此允许把指针定为常量,并且也必须初始化。
int a = 0;
int *const b = &a; // b将一直指向a
const double pi = 3.14;
const double *const pip = π // pip是一个指向常量对象的常量指针
*pip = 2.72; // 错误! pip是一个指向常量的指针
Ⅳ)顶层const: 表示指针(或任意对象)本身是一个常量;底层const:表示指针所指的对象是一个常量。
Ⅴ)constexpr 和常量表达式:常量表达式是指值不会改变并且在编译过程就能得到计算结果的表达式。
使用 constexpr 修改普通变量时,变量必须经过初始化且初始值必须是一个常量表达式。
constexpr int n = 1 + 1;
int u[n] = {1,2};
int url[10];
int url[6 + 4];
int length = 6;
int url[length]; //错误,length是变量
constexpr int mf = 20; // 20是常量表达式
constexpr int limit = mf + 1; // mf + 1 是常量表达式
constexpr int sz = Size(); // 只有当Size是一个constexpr函数时才是一条正确的声明语句
constexpr 可以用于修饰函数的返回值,这样的函数又称为“常量表达式函数”。
// 常量表达式函数:
// 整个函数的函数体中,除了可以包含 using 指令、typedef 语句以及 static_assert 断言外,只能包含一条 return 返回语句。
// 该函数必须有返回值,即函数的返回值类型不能是 void。
// 函数在使用之前,必须有对应的定义语句。普通的函数调用只需要提前写好该函数的声明部分即可(函数的定义部分可以放在调用位置之后甚至其它文件中),但常量表达式函数在使用前,必须要有该函数的定义。
// return 返回的表达式必须是常量表达式
constexpr int sum(int x, int y) {
return x + y;
}
Ⅵ)字面值类型 :算术类型、引用和指针都是字面值类型,而自定义类、IO库、string类型则不属于字面值类型。
Ⅶ)指针和constexpr:在constexpr声明中如果定义了一个指针,限定符constexpr仅对指针有效,与指针所指对象无关。
const int *p = nullptr; // 指向 “整型常量” 的指针
constexpr int *q = nullptr; // 指向整数的 “常量指针”
2.5 处理类型
Ⅰ)类型别名:某种类型的同义词。传统方法是使用关键字typedef,新标准规定了一种新的方法,即使用别名声明using来定义类型的别名。
using SI = Sales_item;
特别要注意含有指针的类型别名,例如:
typedef char *pstring; // char * 和 pstring等价,但不意味着简单的替换。
const pstring cstr = 0; // cstr 是指向char的常量指针。
const pstring *ps; // ps前面的*表示ps是一个指针,它的对象是“指向char的常量指针”。
const char *cstr = 0; // 简单替换的结果:内容不可变为常量,但地址可变的指针变量。而原表达式的意思是常量指针,地址不变。
Ⅱ)auto 类型说明符:编译器替我们分析表达式所属的类型,auto 定义的变量必须有初始值。
auto item = val1 + val2;
auto i = 0, *p = &i;
auto sz = 0, pi = 3.14; // 错误!sz和pi的类型不一样
注意auto对顶层和底层const的作用:
int i = 0, &r = i;
const int ci = i, &cr = ci;
// 忽略顶层const
auto b = ci; // b是一个整数,忽略ci的顶层const特性
auto c = cr; // c是一个整数,忽略cr(ci)的顶层const特性
auto d = &i; // d是一个整型指针(因为&i是整数的地址)
// 保留底层const
auto e = &ci; // e是一个指向整数常量的指针(对常量对象取地址是一种底层const)
// 如果希望auto类型是一个顶层const,可以明确指出(否则还是当成整数)
const auto f = ci;
// 将引用的类型设置为auto
auto &g = ci;
auto &h = 42; // 错误! 不能为非常量引用绑定字面值(非常量引用,可以修改h,但是h绑定的是42)
const auto &j = 42; // 正确,可以为常量引用绑定字面值
Ⅲ)decltype 类型指示符:选择并返回操作数的数据类型。
decltype((variable)) 的结果永远都是引用,decltype(variable)的结果只有当variable本身是一个引用时才是引用。
decltype (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 &
decltype (cj) z; // 错误:z是一个引用,必须初始化
int i = 42, *p = &i, &r = i;
decltype (r+0) b; // b为int型
decltype (*p) c; // 错误!解引用表达式,c的类型为引用,必须初始化,
decltype ((i)) d; // 双括号必为引用
decltype (i) e; // e是一个未初始化的int
2.6 自定义数据结构
Ⅰ)数据结构:一组数据以及相关操作的集合。
Ⅱ)类的定义:以struct或class开始,紧跟着类名和类体,花括号后面必须跟着一个分号。
2.7 其他
定义一般放在头文件中,头文件的书写一般规则:
#ifndef SALES_DATA_H
#define SALES_DATA_H
...
#endif
第 3 章 字符串、向量和数组
3.1 命名空间的using声明
using 声明形式:using namespace :: name
注意:每个名字都需要独立的 using 声明,头文件中不应包含using声明。
using std::cin;
using std::cout; // 只有声明了的名字可以用
using std::endl;
using namespace std; // std里的所有名字都可以用
3.2 标准库类型 string
标准库类型 string 表示可变长的字符序列,使用时必须包含 string 头文件,string 定义在命名空间 std 中。
#include <string>
using std::string
Ⅰ) 定义和初始化string对象
string s1; // 默认初始化为空字符串
string s2 = s1; // s2是S1的副本 拷贝初始化
string s2(s1); // s2是S1的副本 直接初始化
string s3 = "hiya"; // s3是字符串字面值的副本 拷贝初始化
string s3("hiya"); // s3是字符串字面值的副本 直接初始化
string s4(10,'c'); // s4是“cccccccccc” 直接初始化
Ⅱ) string 对象上的操作
os<<s | 将s写到输出流中 |
Is>>s | 从is中读取s,每个字符串以空格分隔 |
getline(is,s) | 从is中读取一行赋给s,包含空格,不包含换行符 |
s.empty() | s为空返回true,否则返回false |
s.size() | 返回s中字符的个数 |
s[n] | 返回s中第n个字符的引用,从0开始 |
s1 + s2 | 返回s1和s2连接后的结果 |
s1 = s2 | 用s2的副本代替s1中原来的内容 |
s1 == s2 | 完全一样则相等,大小写敏感 |
s1 != s2 | - |
<, <=, >, >= | 字典顺序比较,大小写敏感 |
1)读写string对象
#include <string>
using std::string
int main(){
string s; // 空字符串
cin >> s; // 自动忽略开头的空格符、换行符、制表符等空白,将string对象读入s,直到遇到下一个空白停止
cout << s << endl; // 输出s
return 0;
}
2)两个string对象相加,字面值和string对象相加
注意:切记,字符串字面值和string对象是两个不同的类型,并且从左往右看,不能将两个字面值直接相加,如:
// 两个string对象相加
string s1 = "Hello, ", s2 = "world\n";
string s3 = s1 + s2;
s1 += s2;
// 字面值和string对象相加
string s1 = "Hello", s2 = "world";
string s3 = s1 + ", " + s2 + ‘\n’;
string s4 = s1 + ", ";
string s5 = "hello" + ", "; // 错误!两个运算对象都不是string
string s6 = s1 + ", " + "world";
string s7 = "hello" + ", " + s2; // 错误!字面值不能直接相加
3)处理string对象中的字符
cctype头文件:C++标准库中除了定义C++语言特有的功能外,还兼容了C语言的标准库。C语言的头文件是name.h,c++将这些文件命名为cname,去掉了.h后缀,在文件名前面添加了字母c。
isalnum(c) | 检查字符c是否是字母或数字 |
isalpha(c) | 检查字符c是否是字母 |
isblank(c) | 检查字符c是否为空白字符 |
iscntrl(c) | 检查c是否是控制字符 |
isdigit(c) | 检查字符c是否为数字 |
isgraph(c) | 检查字符c是否是具有图形表示的字符 |
islower(c) | 检查c是否是小写字母 |
isprint(c) | 检查c是否是可打印的字符 |
ispunct(c) | 检查c是否是标点符号 |
isspace(c) | 检查c是否是空格字符 |
Ⅲ) 基于范围的for语句
// expression 部分是一个对象,用于表示一个序列。
// declaration 部分负责定义一个变量,该变量用于访问序列中的基本元素。
for ( declaration : expression)
statement
string str("some string");
for (auto c : str)
cout << c << endl;
Ⅳ) 下标运算符:而要想访问string对象中的单个字符,可以使用下标运算符([ ])或者迭代器。
3.3 标准库类型 vector
标准库类型 vector 表示对象的集合,其中所有对象的类型都相同,因为vector容纳着其他对象,所以也常被称为容器。和string类似,要想使用vector,必须包含适当的头文件,并且vector 是模板而非类型,由它生成的模板必须包含vector中元素的类型,因为引用不是对象,所以不存在包含引用的vector。
#include <vector>
using std::vector;
vector<int> ivec;
vector<string> svec;
vector<vector<string>> file; // 老式声明:在里面的vector右边加空格vector<vector<string> >
初始化vector对象的方法:
vector<T> v1; // 默认初始化
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<string> v1{"Hello","World"}; // 列表初始化
vector<string> v1("Hello","World"); // 错误!不能使用()
vector<int> ivec(10,-1); // 10个int类型的元素,每个都被初始化为-1
vector<string> svec(10,"Hello"); // 10个string类型的元素,每个都被初始化为“Hello”
向vector对象中添加元素:利用vector的成员函数push_back向vector中添加元素,push_back负责。
注意:如果循环体内部包含有向vector对象添加元素的语句,则不能使用范围for循环。
v.vector() | 如果v中不含有任何元素,返回真;否则返回假 |
v.size() | 返回v中元素的个数 |
v.push_back(t) | 向v的尾端添加一个值为t的元素 |
v[n] | 返回v中第n个位置上元素的引用 |
v1 = v2 | 用v2中元素的拷贝替换v1中的元素 |
v1 = {a, b, c...} | 用列表中元素的拷贝替换v1中的元素 |
v1 == v2 | 判断v1和v2是否相等 |
v1 != v2 | |
<, <=, >, >= | 以字典顺序进行比较 |
注意:1)要使用size_type,需首先指定它是由哪种类型定义的。vector对象的类型总是包含着元素的类型:
vector<int>::size_type // 正确
vector::size_type // 错误!
2)vector对象以及string对象的下标运算符可用于访问已存在的元素,而不能用于添加元素,只能对确知已存在的元素进行下标操作(确保下标合法的一种有效手段就是尽可能使用范围for语句):
vector<int> ivec; // 空vector对象
cout << ivec[0]; // 错误!ivec不包含任何元素
vector<int> ivec2(10); // 含有10个元素的vector对象
cout << ivec2[10]; // 错误!ivec2元素的合法索引是0~9
3.4 迭代器介绍
迭代器相当于指向容器元素的指针,它在容器内可以向前移动,也可以做向前或向后双向移动。string和vector都支持迭代器,和指针不同的是,获取迭代器不是使用取地址符。
比如,这些类型都拥有名为begin和end的成员,其中begin成员负责返回指向第一个元素的迭代器,end成员负责返回指向容器“尾元素的下一位置”的迭代器(尾后迭代器)。如果容器为空,则begin和end返回的是同一个迭代器。因为end返回的迭代器并不实际指示某个元素,所以不能对其进行递增或解引用的操作。
auto b = v.begin(), e = v.end(); // b表示v的第一个元素,e表示v尾元素的下一位置。
vector<int> v;
const vector<int> cv;
auto it1 = v.begin(); // it1的类型是vector<int>::iterator
auto it2 = cv.begin(); // it2的类型是vector<int>::const_iterator
auto it3 = v.cbegin(); // it3的类型是vector<int>::const_iterator,与vector对象本身是否是常量无关
begin和end返回的具体类型由对象是否是常量决定,如果对象是常量,begin和end返回const_iterator;如果对象不是常量,返回iterator。为了便于专门得到const_iterator类型的返回值,C++11 新标准引用了两个新函数,分别是cbegin和cend,常用于只需读操作无需写操作时。
Ⅰ)迭代器运算符:
*iter | 返回迭代器iter所指元素的引用 |
iter->men | 解引用iter并获取该元素的名为men的成员,等价于(*iter).men |
++iter | 令iter指示容器中的下一个元素 |
--iter | 令iter指示容器中的上一个元素 |
iter1 == iter2 | 判断两个迭代器是否相等,如果两个迭代器指示的是同一个元素或它们是同一个容器的尾后迭代器,则相等;反之,不相等 |
iter1 != iter2 |
Ⅱ) 泛型编程:迭代器有点类似下标运算的意思,C++程序员习惯于使用 !=,其原因和他们更愿意使用迭代器而非下标一样:因为所有标准库容器的迭代器都定义了==和!=,但是它们中的大多数都没有定义<运算符,这种编程风格在标准库提供的所有容器上都有效。
迭代器的含义有3种:1)迭代器本身;2)容器定义的迭代器类型;3)某个迭代器对象。
Ⅲ) 解引用和成员访问操作
(*it).empty() // 解引用it,然后调用结果对象的empty对象
*it.empty() // 错误!试图访问it的名为empty的成员,但it是个迭代器,没有empty成员
it->empty() // 和(*it).empty()效果一样,先解引用,再调用结果对象的对象。
注意:但凡使用了迭代器的循环体,都不要向迭代器所属的容器添加元素。
Ⅳ) 迭代器运算:
iter + n | 向前移动n个位置,还是一个迭代器,指示容器内或尾后位置 |
iter - n | 向后移动n个位置,还是一个迭代器,指示容器内或尾后位置 |
iter1 += n | 迭代器加法的赋值复合语句 |
iter1 -= n | 迭代器减法的赋值复合语句 |
iter1 - iter2 | 两个迭代器之间的距离,必须指向同一个容器中的元素或尾后位置,结果是带符号整数型difference_type |
>、>=、<、<= | 反映迭代器指示位置的前后关系 |
3.5 数组
数组与vector类似,只是数组的大小确定不变,不能随意向数组中添加元素,当不清楚元素的个数时,使用vector。
Ⅰ) 定义和初始化内置数组:
int *ptrs[10]; // []优先级比*高,先看[],知声明的是数组,再看*,数组中装的是指针,即指针数组。
int &refs[10] = "hello world!"; // 同上,但是不存在引用数组,所以这个声明错误
int (*Parray)[10] = &arr; // ()具有最高的优先级,因此先看(),里面是*,声明的是一个指针,再看[],说明该指针指向的是一个数组,即数组指针。
int (&arrRef)[10] = arr; // 同上,()具有最高的优先级,因此先看(),里面是&,声明的是一个引用,再看[],说明该引用指向的是一个数组,即数组引用。
int *(&arry)[10] = ptrs; // 先看(),再看[],再看*。由()知总体是一个引用,再看[]知是一个数组,即含有10个指针元素的数组的引用。
Ⅱ) 访问数组元素: 范围for语句或下标运算符。使用下标运算符的时候要特别注意是否溢出,防止数组下标越界。
Ⅲ) 指针和数组:使用数组的时候,编译器一般会把它转换为指针。
int ia[] = {0,1,2,3,4,5,6,7,8,9};
auto ia2(1a); // ia2是一个整型指针,指向ia的第一个元素,等价于:
auto ia2(&ia[0]);
ia2 = 42; // ia2是一个指针,不能用int型进行赋值
注意,使用decltype关键字时,数组名表示的是整个数组,而不是数组首地址,因此,不能进行上面的赋值。
decltype(ia) ia3 = {0,1,2,3,4,5,6,7,8,9};
ia3 = p; // 错误!不能用整型指针给数组赋值
ia3[4] = i; // 正确,把i的值赋给ia3的一个元素
Ⅳ)标准库函数begin和end:这两个函数和容器的两个同名成员功能类似,不过数组不是类类型,因此这两个函数不是成员函数,使用时需要将数组作为它们的参数。
#include <iterator>
int ia[] = {0,1,2,3,4,5,6,7,8,9};
int *beg = begin(ia); // 指向ia首元素的指针
int *last = end(ia); // 指向尾元素下一个位置的指针
Ⅴ) C风格字符串:建议在C++程序中最好不要使用它们,因为既不方便又容易引发漏洞,而是使用标准库string。
注意:现代的C++程序应当尽量使用vector和迭代器,避免使用内置数组和指针;应该尽量使用标准库string,避免使用C风格的基于数组的字符串。
3.6 多维数组
严格说来,C++ 中并没有多维数组,多维数组其实就是数组的数组。
Ⅰ)使用范围for语句处理多维数组
注意:要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型。
size_t cnt = 0;
for (auto &row : ia)
for (auto &col : row) {
col = cnt;
++cnt;
}
/*
* 注意:在上面的例子中,因为要改变数组元素的值,
* 所以我们选择引用类型作为循环控制变量,还有一个原因:
* 为了避免数组被自动转化成指针。
* 如下面的两个例子:
*/
for (const auto &row : ia)
for (auto col : row) { // 正确
cout << col << endl;
}
// 程序无法编译通过,初始化row时会自动将数组形式的元素
// 转换成指向该数组内首元素的指针,row的类型就是int *。
for (auto row : ia)
for (auto col : row) {
cout << col << endl;
}
本章参考教程: C++ String类型
第 4 章 表达式
4.1 基础
表达式由一个或多个运算对象组成,对表达式求值将得到一个结果。字面值和变量是最简单的表达式,把运算符和运算对象组合起来可以生成较复杂的表达式。
Ⅰ) 重载运算符:当运算符作用于类类型的运算对象时,用户可以自行定义其含义。例如:IO库的>>和<<,string对象、vector对象和迭代器使用的运算符。运算对象的类型和返回值的类型,都是由该运算符定义的;但是运算对象的个数、运算符的优先级和结合律都是无法改变的。
Ⅱ) 左值和右值:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。右值:取不到地址的表达式;左值:能取到地址的表达式。
特别的,如果表达式的求值结果是左值,decltype作用于该表达式(不是变量)得到一个引用类型。例如,对于int *p:
1)因为解引用运算符生成左值,所以decltype(*p)的结果是int&;
2)因为取地址运算符生成右值,所以decltype(&p)的结果是int**。
Ⅲ)<< 运算符没有明确规定何时以及如何对运算对象求值。因此,下面的输出表达式是未定义的。
int i;
cout << i << " " << ++i << endl;
4.2 算术运算符
如果m和n是整数且非0:(m/n)*n + m%n = m
除了-m导致溢出的特殊情况,除法和求余运算满足以下表达式:
(-m)/n = -(m/n) // 负号可以提出
m/(-n) = -(m/n) // 负号可以提出
m%(-n) = m%n // 符号在右边可以忽略
(-m)%n = -(m%n) // 负号在左边可以提出
例如:-21%-8 = -21%8 = -(21%8) = -5
4.3 逻辑和关系运算符
注意逻辑与运算的短路求值:当左侧运算对象为假时,不再计算右侧运算对象。
while(pbeg!=v.end() && *beg>=0)
// 注意:&&两侧运算对象不可交换,即不能变成:
while(*beg>=0 && pbeg!=v.end())
// 否则,当到达尾后时还会进行解运算,报错!
4.4 赋值运算符
int i = 0;
i = 0; // 结果:类型是int,值是0
i = 3.14; // 结果:类型是int,值是3
i = {3.14}; // 错误!窄化转化
4.5 递增和递减运算符
Ⅰ) 除非必须,否则不用递增递减运算符的后置版本。
Ⅱ)*p++ 等价于 *(p++)
4.6 成员访问运算符
ptr->mem 等价于(*ptr).mem。
4.7 条件运算符
Ⅰ)条件运算符满足右结合律。
Ⅱ)输出表达式中使用条件表达式。
cout << ((grad < 60) ? "fail" : "pass"); // 输出pass或fail
cout << (grad < 60) ? "fail" : "pass"; // 输出1或0
cout << grad < 60 ? "fail" : "pass"); // 错误!试图比较cout和60的值
4.8 sizeof运算符
sizeof运算符:返回一条表达式或一个类型名字所占的字节数,满足右结合律,得到的值的类型是size_t。sizeof不会实际求运算对象的值,所以即使运算对象是一个无效指针也不会影响。
4.9 类型转换
C++强制类型转换:在C++语言中新增了四个关键字static_cast、const_cast、reinterpret_cast和dynamic_cast。这四个关键字都是用于强制类型转换的,新类型的强制转换可以提供更好的控制强制转换过程,允许控制各种不同种类的强制转换,它们能更清晰的表明它们要干什么,程序员能立即知道一个强制转换的目的。
参考教程:C++ 四种强制类型转换
第 5 章 语句
5.1 简单语句
Ⅰ)使用空语句时应该加上注释,从而令读这段代码的人知道该语句是有意省略的。
Ⅱ)别漏写分号,也别多写分号。因为多余的空语句并非总是无害的,可能会影响循环语句的逻辑。
5.2 语句作用域
在if、switch、while和for语句的控制结构内部定义的变量,只在相应语句的内部可见,一旦语句结束,变量将超出作用范围,无法再访问。
5.3 条件语句
Ⅰ)if语句
// if语句
if (condition)
statement
// if-else语句
if (condition)
statement1
else
statement2
规定:else与离它最近的尚未匹配的if匹配。
Ⅱ)switch语句
1)case标签必须是整型表达式, 如:case 3.14:
2)switch语句最后的break语句最好加上,既可增加程序可读性,又方便以后增加新的case分支。
3)标签不应该孤零零地出现,后面必须跟上一条语句或另一个case标签。
5.4 迭代语句
Ⅰ)while语句
1)当不确定到底要迭代多少次,以及在循环结束后访问循环控制变量时,使用while循环比较合适。
2)注意定义在while的条件部分或循环体内部的变量每次都要经历从创建到销毁的过程。
Ⅱ)for语句
1)传统的for语句
// 传统for语句语法形式:
for (①init-ststement; ②condition; ③expression)
④statement;
// 执行顺序:① → ② → ④ → ③ → ② → ④ → ...
注意:init-ststement 也可以定义多个对象,但是只能有一条语句,因此所有的变量的基础类型必须相同。
2)范围for语句
// 范围for语句语法形式
for (declaration:expression)
statement
其中,declaration定义一个变量,确保类型相容最简单的方法是使用auto类型说明符,并且如果需要对序列中的元素执行写操作,循环变量必须声明为引用类型。expression表示的必须是一个序列,比如:用花括号括起来的初始列表、数组、vector或string等类型的对象,这些类型的共同特点是拥有能返回迭代器的begin和end成员。
Ⅲ)do while语句
与while语句的区别是,不论条件是否满足,至少执行一次循环。
注意:do while语句应该在括号包围起来的条件后面用一个分号表示语句结束。
5.5 跳转语句
Ⅰ) break语句
作用:负责终止离它最近的while、do while、for或switch语句,并从这些语句之后的第一条语句继续执行。
Ⅱ) continue语句
作用:终止最近的循环中的当前迭代并立即开始下一次迭代,注意仍然会继续执行循环。
Ⅲ) goto语句
注意:不要在程序中使用goto语句。
5.6 try语句块和异常处理
程序的错误大致可以分为三种,分别是语法错误、逻辑错误和运行时错误:
1) 语法错误在编译和链接阶段就能发现,只有 100% 符合语法规则的代码才能生成可执行程序。语法错误是最容易发现、最容易定位、最容易排除的错误,程序员最不需要担心的就是这种错误。
2) 逻辑错误是说我们编写的代码思路有问题,不能够达到最终的目标,这种错误可以通过调试来解决。
3) 运行时错误是指程序在运行期间发生的错误,例如除数为 0、内存分配失败、数组越界、文件不存在等。C++异常(Exception)机制就是为解决运行时错误而引入的。
Ⅰ) throw表达式
程序的异常检测部分使用throw表达式引发一个异常,throw表达式包含关键字throw和紧随其后的一个表达式,其中表达式的类型就是抛出的异常类型,后面再紧跟一个分号,从而构成一条表达式语句。例如:
if (item1.isbn() != item2.isbn())
throw runtime_error("Data must refer to same ISBN")
其中,类型runtime_error是标准库异常类型的一种,定义在stdexcept 头文件中。抛出异常将终止当前函数,并把控制权转移给能处理该异常的代码。
Ⅱ) try语句块
try语句块的通用语法形式是:
try {
program-sataments
} catch (exception-declaration){
handler-statements
} catch (exception-declaration){
handler-statements
} // ...
try 中包含可能会抛出异常的语句,一旦有异常抛出就会被后面的 catch 捕获。从 try 的意思可以看出,它只是“检测”语句块有没有异常,如果没有发生异常,它就“检测”不到。catch 是“抓住”的意思,用来捕获并处理 try 检测到的异常;如果 try 语句块没有检测到异常(没有异常抛出),那么就不会执行 catch 中的语句。
#include <iostream>
#include <string>
#include <exception>
using namespace std;
int main() {
string str = "Hello World!";
try {
char ch1 = str[100];
cout << ch1 << endl;
}catch (exception e) {
cout << "[1]out of bound!" << endl;
}
try {
char ch2 = str.at(100);
cout << ch2 << endl;
}catch (exception &e) { //exception类位于<exception>头文件中
cout << "[2]out of bound!" << endl;
}
return 0;
}
运行结果:
[2]out of bound!
可以看出,第一个 try 没有捕获到异常,输出了一个没有意义的字符(垃圾值)。因为[ ]
不会检查下标越界,不会抛出异常,所以即使有错误,try 也检测不到。换句话说,发生异常时必须将异常明确地抛出,try 才能检测到;如果不抛出来,即使有异常 try 也检测不到。所谓抛出异常,就是明确地告诉程序发生了什么错误。
第二个 try 检测到了异常,并交给 catch 处理,执行 catch 中的语句。需要说明的是,异常一旦抛出,会立刻被 try 检测到,并且不会再执行异常点(异常发生位置)后面的语句。本例中抛出异常的位置是 at() 函数,它后面的 cout 语句就不会再被执行,所以看不到它的输出。
说得直接一点,检测到异常后程序的执行流会发生跳转,从异常点跳转到 catch 所在的位置,位于异常点之后的、并且在当前 try 块内的语句就都不会再执行了;即使 catch 语句成功地处理了错误,程序的执行流也不会再回退到异常点,所以这些语句永远都没有执行的机会了。本例中,第 18 行代码就是被跳过的代码。
执行完 catch 块所包含的代码后,程序会继续执行 catch 块后面的代码,就恢复了正常的执行流。
Ⅲ)标准异常
图片引自:C++ exception类:C++标准异常的基类
第 6 章 函数
6.1 函数基础
① 一个典型的函数包括:返回类型、函数名字、形参列表以及函数体,执行函数使用调用运算符,即一对圆括号。函数的调用完成两项工作:一是实参初始化函数对应的形参,二是将控制权转移给被调用的函数。
注意:调用运算符(())的优先级与点运算符和箭头运算符相同,并且也符合左结合律。例如:
auto sz = shorterString(s1, s2).size();
// shorterString的结果是点运算符的左侧运算对象,点运算符可以得到该string对象的size成员,size又是第二个调用运算符的左侧运算对象。
#include <iostream>
#include <vector>
#include <list>
#include <string>
using namespace std;
int main(int argc,char** argv)
{
cout<<"+*a[2]:"<<+*a[2]<<endl; // *(解引用)运算符优先级与一元正号+运算符相同,皆为右结合律
int c[3]= {1,3,5};
vector<int> b(c,c+3);
auto p = b.begin(); // *(解引用)运算符的优先级低于后置递增运算符(++),但是结果为1,后置++返回的是原对象的副本,并不会将其递增的结果返回
cout<<"*p++:"<<*p++<<endl;
cin.get();
return 0;
}
② 实参的求值顺序没有规定,编译器可以以任何可行的顺序对实参求值。
③ 注意:即使两个形参类型一样,也必须把两个类型都写出来,并且任意两个形参都不能重名。
④ 函数的返回值类型不能是数组类型或函数类型,但是可以是指向数组或函数的指针。
⑤ 局部静态对象:在程序的执行路径第一次经过对象定义语句时初始化,并且直到程序终止才被销毁。例如:
size_t count_calls()
{
static size_t ctr = 0; // 函数调用结束后,ctr仍然有效,直到main函数执行完毕
return ++ctr;
}
int main()
{
for (size_t ctr = 0; i != 10; ++i)
cout << count_calls() << endl;
return 0;
}
⑥ 函数只能定义一次,但是可以声明多次。建议在头文件中对函数进行声明,在源文件中进行定义。 含有函数声明的头文件应该包含到定义函数的源文件中。
6.2 参数传递
① C++语言中,建议使用引用类型的形参替代指针。拷贝大的类类型对象或者容器对象比较低效,甚至有的类类型根本就不支持拷贝操作。如果函数无需改变引用形参的值,最好将其声明为常量引用。
② 使用引用形参返回额外信息:一个函数只能返回一个值,然而有时函数需要同时返回多个值,引用形参为我们一次返回多个结果提供了有效途径。例如:
/*
* 功能描述:
* 返回s中c第一次出现的位置索引
* 引用形参occurs负责统计c出现的总次数
*/
// 只读不写,用const
string::size_type find_char (const string &s, char c, string::size_type &occurs)
{
auto ret = s.size(); // 用来保存第一次出现的位置
occurs = 0; // 出现次数
for (decltype(ret) i = 0; i != s.size(); ++i) {
if (s[i] == c)
if (ret == s.size())
ret = i; // 记录第一次出现的位置
++occurs; // 将出现的次数加一
}
return ret; // 出现的次数通过occurs隐式地返回
}
// 调用:
auto index = find_char(s, 'o', ctr);
*③ const 形参和实参:用实参初始化形参时会忽略掉顶层const,即形参的顶层const被忽略掉了,当形参有顶层const时,传给它常量对象或者非常量对象都是可以的。
void fcn(const int i) { } // fcn能够读取i,但是不能向i写值
void fcn(int i) { } // 错误,重复定义fcn
第二个fcn是错误的,尽管形式上有差异,但实际上它的形参和第一个fcn的形参没什么不同。
* ④ 指针或引用形参与const
形参的初始化和变量的初始化是一样的,可以用非常量初始化一个底层const,但是反过来不行;同时一个普通的引用必须用同类型的对象初始化。例如:
void reset(int *ip) { }
void reset(int &i) { }
string::size_type find_char(const string &s, char c, string::size_type &occurs)
int main()
{
int i;
const int ci;
string::size_type ctr = 0;
//*****************************************************************
reset(&i); // 调用形参类型是int*的reset函数
reset(&ci); // 错误!不能将一个指向const对象的指针传递给一个普通指针。这里这样初始化,存在用ip修改const变量ci的值的风险。
//*****************************************************************
reset(i); // 调用形参类型是int&的reset函数
reset(ci); // 错误!不能将一个指向const对象的引用传递给一个普通引用。这里这样初始化,存在用i修改const变量ci的值的风险。
reset(42); // 错误!存在用i修改42的值的风险,但实际上42是个常量,除非函数改为void reset(const int &i) { }
//*****************************************************************
reset(ctr); // 错误!类型不匹配,ctr是一个无符号类型
find_char("Hello World!", 'o',ctr);
// 正确:find_char的第一个形参是对常量的引用
}
⑤ 数组形参:数组是以指针的形式传递给函数的,所以一开始函数并不知道数组的确切尺寸,调用者应该为此提供一些额外信息。管理指针形参有三种常见的技术:使用标记指定数组的长度、
void print(const char *cp)
{
if (cp)
while (*cp)
cout << *cp++ <<endl;
}
使用标准库规范、
void print(const int *beg, const int *end)
{
while(beg != end)
cout << *beg++ << endl;
}
显式传递一个表示数组大小的形参
void print(const int ia[], size_t size)
{
for (size_t i = 0; i != size; ++i) {
cout << ia[i] << endl;
}
}
⑥ main:处理命令行选项:有时我们需要给main函数传递实参,一种常见的情况是用户通过设置一组选项来确定函数所要执行的操作。
int main(int argc, char *argv[]) { }
// 或
int main(int argc, char **argv) { }
// 第一个形参argc表示数组中的字符串数量,第二个形参argv是一个数组
// 若传递下面的选项:prog -d -o ofile data0
/*
* argc = 5,
* argv[0] = "prog",
* argv[1] = "-d",
* argv[2] = "-o",
* argv[3] = "ofile",
* argv[4] = * "data0",
* argv[5] = "0"。
* 注意:可选实参从argv[1]开始,argv[0]保存程序的名字,而非用户输入。
*/
⑦ 含有可变形参的函数:C++新标准提供了两种主要方法:1)如果所有的实参类型相同,可以传递一个名为initializer_list的标准库类型。如果实参的类型不同,可以编写一种特殊的函数——可变参数模板。
initializer_list<string> ls;
initializer_list<int> li;
void error_msg(initializer_list<string> il)
{
for (auto beg = il.begin(); beg != il.end(); ++beg)
cout << *beg << " ";
cout << endl;
}
注意:如果想向initializer_list形参中传递一个值的序列,则必须把序列放在一对花括号内。含有initializer_list形参的函数也可以同时含有其他形参。
error_msg({"functionx","okay"});
省略符形参:为了方便C++程序访问某些特殊的C代码而设置的,省略符形参不应用于其他目的,且只能出现在形参列表的最后一个位置,例如:
void foo(parm_list, ...);
void foo(...);
6.3 返回类型和return语句
① 有返回值函数:含有return语句的循环后面应该也有一条return语句,如果没有的话程序就是错误的,并且很多编译器都无法发现此类错误。
② 不要返回局部对象的引用或指针:
const string &manip()
{
string ret;
if (!ret.empty())
return ret; // 错误!返回局部对象的引用
else
return "Empty"; // 错误!"Empty"是一个局部临时值
}
③ 调用一个返回引用的函数得到左值,其他返回类型得到右值。特别地,可以为返回类型是非常量引用的函数的结果赋值,如:
char &get_val(string &str, string::size_type ix)
{
return str[ix];
}
int main()
{
string s("a value");
cout << s << endl;
get_val(s,0) = 'A'; // 可以为返回类型是非常量引用的函数的结果赋值
cout << s << endl;
return 0;
}
④ 列表初始化返回值:C++11新标准规定,函数可以返回花括号包围的值的列表。
vector<string> process()
{
// expected、actual是string对象
if (expected.empty())
return {};
else if (expected == actual)
return {"functionx", "okay"}
else
return {"functionx", expected, actual}
}
⑤ 主函数main的返回值:允许main函数没有return语句直接结束,编译器会在执行到结尾处隐式地插入一条返回0的return语句。main函数返回0表示执行成功,其他值表示失败,非0值具体含义依机器而定。
⑥ 函数递归:函数直接或者间接调用它自身。如:
int factorial(int val)
{
if (val > 1)
return factorial(val - 1) * val;
return 1;
}
⑦ 返回数组指针:定义一个返回数组的指针或者引用的函数比较麻烦,但是可以使用类型别名来简化这一任务。
// 使用类型别名
using arrT = int[10]; // 等价于typedef int arrt[10]
arrT* func (int i); // func返回一个指向含有10个整数的数组的指针
// 不使用类型别名
int (*func(int i)) [10];
/*
* 对于上述表达式的理解:从内到外按顺序理解
* func(int i)表示调用该函数时,需要一个int类型的实参。
* (*func(int i))表示对调用结果解引用
* (*func(int i)) [10]表示解引用后得到一个大小是10的数组
* int (*func(int i)) [10]表示数组中的元素类型是int类型
*/
// 使用尾置返回类型
auto func(int i) -> int(*) [10] // 把函数返回类型放在形参列表之后,可以知道函数返回的是一个指针,该指针指向含有10个整数的数组
// 使用decltype
int odd[] = {1, 3, 5, 7, 9};
int even[] = {2, 4, 6, 8};
decltype(odd) *arrPtr(int i) // 返回一个指针,该指针指向含有5个整数的数组
{
return (i % 2) ? &odd : &even; // 返回一个指向数组的指针
}
6.4 函数重载
① 函数重载:同一作用域内的几个函数名字相同但是形参列表不同,注意main函数只能有一个,不能重载。
Record lookup(Phone);
Record lookup(const Phone); // 重复声明
Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明
Record lookup(Account&);
Record lookup(const Account&); // 新函数
Record lookup(Account*);
Record lookup(const Account*); // 新函数
② 不允许两个函数除了返回类型外其他所有的要素都相同。如:
Record lookup(const Account&)
bool lookup(const Account&) // 错误!与上一个函数只有返回类型不同
③ const_cast和重载。例如:
/*
* 解读:函数Ⅰ的参数和返回类型都是const string的引用,当我们用两个非
* 常量string实参调用这个函数时,返回的结果将还是const string的引用;
* 当我们用两个非常量string实参调用,返回的结果却要是一个普通的引用时,
* 我们可以定义新的函数:函数Ⅱ。
*/
// 函数Ⅰ
const string &shorterString(const string &s1, const string &s2)
{
return s1.size() <= s2.size() ? s1 : s2;
}
// 函数Ⅱ
string &shorterString(string &s1, string &s2)
{
auto &r = shorterString(const_cast<const string>(s1), const_cast<const string>(s2));
return const_cast<string&>(r);
}
④ 重载与作用域:在内层作用域声明名字,将会隐藏外层作用域中声明的同名实体。
6.5 特殊用途语言特性
① 默认实参:默认实参作为形参的初始值出现在形参列表中。
注意:1) 一旦某个形参被赋予了初始值,那么它后面的所有的形参都必须有默认值。2) 尽量让不怎么使用默认值的形参出现在前面,让经常使用默认值的形参出现在后面。3) 不能修改一个已经存在的默认值,但是可以添加默认值。
② 内联函数:内联函数可以避免函数调用的开销。内联说明只是向编译器发出一个请求,编译器可以选择忽略这个请求。
③ constexpr函数:函数的返回类型和所有的形参类型都必须是字面值类型,函数体中必须有且只有一条return语句,constexpr函数被隐式地指定为内联函数。constexpr函数不一定返回常量表达式。
注意:内联函数和constexpr函数通常定义在头文件中。
④ 调试帮助:开发过程中使用的代码,发布时可屏蔽。
assert预处理宏常用于检查“不能发生”的条件。
// 一个对输入文本进行操作的程序可能要求所有给定单词的长度都大于某个阈值。
assert(word.size() > threshold);
可以用NDEBUG使得assert无效,如:
#define NDEBUG // 关闭调试状态,必须在cassert头文件上面。
#include <cassert>
int main(void)
{
int x = 0;
assert(x);
}
6.6 函数匹配
① 确定候选函数和可行函数。
② 寻找最佳匹配。
6.7 函数指针
bool (*pf) (const string &, const string &);
pf指向了一个函数,该函数的参数是两个const string的引用,返回值是bool类型。
注意:*pf两端的括号必不可少,如果不写,则表示pf是一个返回值为bool指针的函数。
① 和数组类似,当我们把函数名作为一个值使用时,该函数自动地转换成指针。
② 函数和指针地类型必须精确匹配。
③ 和函数类型的形参不一样,返回类型不会自动转换成指针,必须显式地将返回类型指定为指针。
④ 如果明确知道返回的函数是哪一个,就可以使用decltype简化书写函数指针返回类型的过程。
第七章 类
前言
类的基本思想是数据抽象和封装。数据抽象是一种依赖于接口和实现分离的编程技术。类的接口包括用户所能执行的操作;类的实现包括类的数据成员、负责接口实现的函数体以及定义类所需的各种私有函数。
封装实现了类的接口和实现的分离。封装后隐藏了类的实现细节,用户只能使用接口而无法访问实现部分。类要实现数据抽象和封装,需要首先定义一个抽象数据类型(ADT)。
7.1 定义抽象数据类型
Sales_data的接口包括:isbn成员函数、combine成员函数、add函数、read函数和print函数。
Ⅰ) 使用改进的Sales_data类
Sales_data total;
if (read(cin, total)) {
Sales_data trans;
while (read(cin, trans)) {
if (total.isbn() == trans.isbn())
total.combine(trans);
else {
print(cout, total) << endl;
total = trans;
}
}
print(cout, total) << endl;
} else {
cerr << "No data?!" <<endl;
}
/*
* read函数将第一条交易数据读入total中,如果读入失败,程序直接跳转到else语句并输出一条错误信息
* 如果检测到读入了数据,将每一条交易保存到trans中,while同样检测读入操作是否成功以判断新的交易。
*/
Ⅱ)定义改进的Sales_data类
//==============================================================================
struct Sales_data {
// 关于Sales_data对象的操作,isbn定义在类内是隐式的inline函数,combine和avg_price定义在类外。
std::string isbn() connst { return bookNo; }
/*
* 在成员函数isbn内部,我们可以直接调用该函数的对象的成员(bookNo),而无须通过成员
* 访问运算符来做到这一点,任何对类成员的直接访问都被看做this的隐式引用。因此:
* return bookNo; 等价于 return this -> bookNo; this是一个常量指针,不允许改变
* this中保存的对象,这里this的类型是(Sales_data * const),下面伪代码进行说明:
* std::string Sales_data::isbn(const Sales_data *const this) { return this -> isbn; }
*
* 这里的const表示isbn是一个常量成员函数:类的成员函数后面加const,表明这个函数不会
* 修改传入的类对象的数据成员。在这里的作用是修改隐式this指针的类型,因为this默认情况
* 下是指向类类型非常量版本的常量指针,这里相当于修改为:const Sales_data *const this
*/
Sales_data& combine(const Salea_data&); // const表示对传入的类对象不做改变
double avg_price() const;
// 数据成员
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
//==============================================================================
// 非成员接口函数声明
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
//==============================================================================
// 成员接口函数外部定义。
double Sales_data::avg_price() const {
if (units_sold)
return revenue/units_sold;
else
return 0;
}
/*
* total.combine(trans); 调用该函数的对象total代表的是赋值运算符左侧的运算对象,
* 右侧运算对象通过显式的实参trans被传入函数。其中,total的地址被绑定到隐式的this
* 参数上,所以最后可以通过*this返回。
* 左值返回:使用Sales_data&
*/
// rhs:right hand side
Sales_data& Sales_data::combine(const Salea_data&rhs) {
units_sold += rhs.units_sold;
revenue += rhs.revenue;
return *this;
}
//==============================================================================
/*
* 类相关的非成员函数定义和声明都在类外。输入的交易信息包括ISBN、售出总数和售出价格。
* 一般来说,如果非成员函数是类接口的组成部分,则应该与类在同一个头文件中声明。
* 注意:
* 1)IO类型不可拷贝,只能通过引用来传递。读取和写入会改变流的内容,使用普通引用而非常量引用。
* 2)print函数不负责换行。执行输出任务的函数应该尽量减少对格式的控制,由用户代码决定是否换行。
*/
istream &read(istream &is, Sales_data &item) {
double price = 0;
is >> item.bookNo >> item.units_sold >> price;
item.revenue = price * item.units_sold;
return is;
}
ostream &print(ostream&os, const Sales_data&item) {
os << item.isbn() << " " << item.units_sold << " " << item.revenue <<" " << item.avg_price();
return os;
}
Sales_data add(const Sales_data&lhs, const Sales_data&rhs) {
Sales_data sum = lhs;
sum.combine(rhs);
return sum;
}
//==============================================================================
注意:
- 成员函数的声明必须在类的内部,它的定义既可以在类的内部也可以在类的外部,定义在类的内部时,函数是隐式的inline函数;定义在类的外部时,需加上类名和域作用符:: 。
- 作为接口组成部分的非成员函数,例如add、read和print等,它们的定义和声明都在类的外部。
- 成员函数体可以随意使用类中的其他成员而无需在意这些成员出现的顺序,因为编译器首先编译成员的声明,然后才轮到成员函数体。
Ⅲ) 构造函数:构造函数的任务是初始化类对象的数据成员,无论何时主要类的对象被创建,就会执行构造函数。
构造函数的名字和类名相同,与普通函数相比没有返回类型。类包含多个构造函数时可以进行重载。在没有定义任何构造函数时,编译器将通过隐式地定义默认构造函数进行默认初始化,编译器创建的构造函数又称为合成的默认构造函数。
对于某些类来说,合成的默认构造函数可能执行错误的操作。含有内置类型或复合类型(比如数组和指针)成员的类,应该在类的内部全部初始化这些成员,才适用于使用合成的默认构造函数,或者,直接定义一个自己的默认构造函数。有的时候,编译器不能为某些类合成默认的构造函数。例如:如果类中包含一个其他类类型的成员且这个成员的类型没有默认的构造函数,编译器将无法初始化该成员。
struct Sales_data {
// 新增的构造函数
Sales_data() = default; // 默认构造函数
Sales_data(const std::string &s) : bookNo(s) { } // 等价于Sales_data(const std::string &s) : bookNo(s),units_sold(0),revenue(0) { }
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s) , units_sold(n), revenue(p*n){ }
Sales_data(std::istream &);
// 之前已有的其他成员
std::string isbn() const { return bookNo; }
Sales_data& combine(const Salea_data&);
double avg_price() const;
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
注意:① 构造函数不应该轻易覆盖掉类内的初始值,除非新赋的值与原值不同。
② 构造函数不能声明成const的,const在函数的后面,表示不能修改成员变量,但构造函数一般或者总是要修改成员变量的。const对象不能调用非const成员函数,并且构造函数是用来初始化类的,初始化就是给对象维护的变量进行初始化,哪怕对象或类里面没有变量,构造函数的功能依然是初始化,既然是初始化,编译器就会认为你“可能”会初始化变量,所以编译器不让你的代码通过。这也是编译器的一个规定。
③ 创建一个类的const对象时,直到构造函数完成初始化对象才真正获得“常量”属性。因此,构造函数在const对象的构造过程中可以向它写值。
④ 只有当类没有声明任何构造函数的时候,编译器才会自动的生成默认构造函数。
在类的外部定义构造函数:
Sales_data::Sales_data(std::istream &is) {
read(is, *this); // 使用this来把对象当成一个整体访问
}
// 首先使用了域作用符,说明定义的是Sales_data类的成员;又因为成员名和类名相同,所以它是一个构造函数。
// 尽管这个构造函数初始值列表是空的,但是它通过执行构造函数体初始化对象的成员。
Ⅳ) 拷贝、赋值和析构 :如果类包含vector或者string成员,则其拷贝、赋值和销毁的合成版本能够正常工作。
7.2 访问控制与封装
Ⅰ) 在C++语言中,使用访问说明符加强类的封装性:定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口。定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了类的实现细节。每个访问说明符指定了接下来的成员的访问级别,其有效范围直到出现下一个访问说明符或者到达类的结尾处为止。
// 这里开始使用class(注意不是Class)关键字定义类,与struct的唯一区别就是默认访问权限不同:
// 在没有访问说明符时,class默认全部都是private,而struct默认全部都是public
class Sales_data {
public:
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s) , units_sold(n), revenue(p*n){ }
Sales_data(std::istream &);
std::string isbn() connst { return bookNo; }
Sales_data& combine(const Salea_data&);
private:
double Sales_data::avg_price() const {
return units_sold ? revenue/units_sold : 0;
}
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
Ⅱ) 友元:类可以允许其它类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。如果类想把一个函数作为它的友元,只需增加一条以friend关键字开头的函数声明语句即可。
class Sales_data {
// 友元声明只能出现在类定义的内部,但是位置不限。
friend Sales_data add(const Sales_data&, const Sales_data&);
friend std::ostream &print(std::ostream&, const Sales_data&);
friend std::istream &read(std::istream&, Sales_data&);
public:
Sales_data() = default;
Sales_data(const std::string &s) : bookNo(s) { }
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s) , units_sold(n), revenue(p*n){ }
Sales_data(std::istream &);
std::string isbn() connst { return bookNo; }
Sales_data& combine(const Salea_data&);
private:
std::string bookNo;
unsigned units_sold = 0;
double revenue = 0.0;
};
// 非成员函数声明,注意必须要有。
Sales_data add(const Sales_data&, const Sales_data&);
std::ostream &print(std::ostream&, const Sales_data&);
std::istream &read(std::istream&, Sales_data&);
类还可以把其他类定义成友元,也可以把其他类(之前已定义的)的成员函数定义成友元。友元可以定义在类的内部,这样函数是隐式内联的。
class Screen {
frend class Window_mgr; // Window_mgr的成员可以访问Screen类的私有部分。
};
class Window_mgr() {
public:
using ScreenIndex = std::vector<Screen>::size_type;
void clear(ScreenIndex );
private:
std::vector<Screen> screens{screen(24, 80, ' ')};
};
void Window_mgr::clear(ScreenIndex i) {
Screen &s = screens[i];
s.contents = string(s.height * s.width, ' '); // 可以访问Screen类的height, width, contents。
}
注意:友元不具有传递性。例如,C是B的友元,B是A的友元,不能直接说明C也是A的友元。
除了令整个类作为友元外,还可以只为类的某一成员函数提供访问权限:
class Screen {
friend void Window_mgr::clear(ScreenIndex);
};
关于友元的程序设计方式:
1)定义Window_mgr类,声明clear函数,但是不能定义它。在clear使用Screen的成员之前必须先声明Screen。
2)定义Screen,包括对于clear的友元声明。
3)定义clear,此时可以使用Screen的成员。
注意:1)对于重载函数,尽管它们名字一样,但仍然属于不同的函数,如果一个类要想把重载函数声明为它的友元,需要对每一个函数分别声明。2)友元声明的作用仅仅是影响访问权限,本身不是普通意义上的声明。
struct X {
friend void f() { /* 友元函数可以定义在类的内部 */}
X() {
f(); // 错误!f还未声明
}
void g();
void h();
};
void X::g() { return f(); } // 错误!f还未声明
void f(); // 开始声明f
void X::h() { return f(); } // 正确!此时f声明在作用域中了
7.3 类的其他特性
Ⅰ)类成员再探
class Screen {
public:
typedef std::string::size_type pos;
// 等价语句:
using pos = std::string::size_type;
// 注意:用来定义类型的成员必须先定义,后面再使用。
// 因此,类型成员通常出现在类开始的地方。
Screen() = default; // 因为下面有构造函数,所以编译器不会自动生成默认构造函数,需要自己写出来。
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c) { }
char get() const { return contents[cursor]; } // 隐式内联:读取光标处的字符
inline char get(pos ht, pos wd) const; // 显式内联,是上面get()函数的重载函数,编译器根据实参数量决定调用哪个。
Screen &move(pos r, pos c);
void some_member() const;
private:
pos cursor = 0; // 光标的位置
pos height = 0, width = 0; // 屏幕的高和宽
std::string contents; // 屏幕的内容
mutable size_t access_ctr; // 可变数据成员:添加mutable关键字,即使在一个const对象内也能被修改
};
inline Screen &Screen::move(pos r, pos c) { // 显式内联
pos row = r * width;
cursor = row + c;
return *this;
}
char Screen::get(pos r, pos c) const { // 隐式内联
pos row = r * width;
return contents[row + c];
}
void Screen::some_member() const {
++access_ctr; // 记录成员函数被调用次数
}
Ⅱ)类数据成员的初始值:
class Window_mgr() {
private:
std::vector<Screen> screens { Screen(24, 80, ' ') }; // 用{ }进行直接初始化
};
Ⅲ)返回*this的成员函数:
继续往Screen类中添加成员函数:
class Screen {
public:
typedef std::string::size_type pos;
Screen() = default;
Screen(pos ht, pos wd, char c) : height(ht), width(wd), contents(ht * wd, c) { }
char get() const { return contents[cursor]; }
inline char get(pos ht, pos wd) const;
Screen &move(pos r, pos c);
void some_member() const;
Screen &set(char); // √
Screen &set(pos, pos, char); // √
private:
pos cursor = 0;
pos height = 0, width = 0;
std::string contents;
mutable size_t access_ctr;
};
inline Screen &Screen::set(char c) {
contents[cursor] = c; // 设置当前光标所在位置的新值
return *this; // 将this对象作为左值返回
}
inline Screen &Screen::set(pos r, pos col, char ch) {
contents[r * width + col] = ch; // 设置给定位置的新值
return *this; // 将this对象作为左值返回
}
7.4 类的作用域
在类的作用域之外,普通的数据和函数成员只能由对象、引用或者指针使用成员访问运算符来访问;对于类类型成员,则使用作用域运算符访问。
Screen::pos ht = 24, wd = 80; // 使用Screen定义的pos类型
Screen scr(ht, wd, ' ');
Screen *p = &scr;
char c = scr.get(); // 访问scr对象的get成员
c = p -> get(); // 访问p所指对象的get成员
Ⅰ)一个类就是一个作用域,在类的外部定义成员函数时必须同时提供类名和函数名,因为在类的外部,成员的名字被隐藏起来了。
Ⅱ)而非函数的返回类型通常出现在函数名之前,因此,当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外。这时,返回类型必须指明它是哪个类的成员。例如:
class Window_mgr() {
public:
ScreenIndex addScreen(const Screen&);
};
Window_mgr::ScreenIndex Window_mgr::addScreen(const Screen &s) {
screens.push_back(s);
return screens.size() - 1;
}
Ⅲ)编译器处理完类中的全部声明后才会处理成员函数的定义。
typedef double Money;
string bal;
class Account {
public:
Money balance() { return bal; } // 注意:这里返回的bal是类内的Money类型的成员,而不是类外的string类型
private:
Money bal;
};
Ⅳ)如果成员使用了外层作用域中的某个名字,而该名字代表一种类型,则类不能在之后重新定义该名字。
typedef double Money;
class Account {
public:
Money balance() { return bal; } // 使用外层作用域的Money
private:
typedef double Money; // 错误!不能重新定义Money,即使类型一致也不行
Money bal;
};
Ⅴ)不建议使用其他成员的名字作为某个成员函数的参数。
int height; // 定义了一个名字,Screen将使用
class Screen {
public:
typedef std::string::size_type pos;
void dummy_fcn(pos height) {
cursor = width * height; // 不建议的写法:函数的参数和类成员使用了相同的名字。这里将作为函数内的形参声明
/*
* 不建议的写法:
* void Screen::dummy_fcn(pos height) {
* cursor = width * this -> height; // 成员height,显式地使用this指针来强制访问成员
* // 或者:
* // cursor = width * Screen::height; // 成员height,通过加上类的名字来强制访问成员
*/
}
private:
pos cursor = 0;
pos height = 0, width = 0;
};
Ⅵ)当成员定义在类的外部时,名字查找的第三步不仅要考虑类定义之前的全局作用域中的声明,还要考虑在成员函数定义之前的全局作用域中的声明。
int height; // 定义了一个名字,Screen中将使用
class Screen {
public:
typedef std::string::size_type pos;
void setHeight(pos);
pos height = 0; // 隐藏了外层作用域中的height
};
Screen::pos verify(Screen::pos); // verify的声明位于setHeight定义之前,可以被正常使用
void Screen::setHeight(pos var) {
height = verify(var); // var:参数;height:类的成员;verify:全局函数
}
7.5 构造函数再探
Ⅰ)在定义变量时,我们习惯于立即对其进行初始化,而非先定义再赋值,对于对象的数据成员而言,初始化和赋值也有类似的区别。
Ⅱ)如果成员是const或者是引用的话,必须将其初始化。当成员属于某种类类型且该类没有定义默认构造函数时,也必须将这个成员初始化。如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,必须通过构造函数初始化列表显式地初始化。建议养成使用构造函数初始值的习惯,这样可以避免很多意想不到的编译错误,特别是当有的类含有需要构造函数初始值的成员时。
class ConstRef {
public:
ConstRef(int ii);
private:
int i;
const int ci;
int &ri;
};
// 错误!
ConstRef::ConstRef(int ii) { // 构造函数
i = ii; // 正确
ci = ii; // 错误!不能给const赋值
ri = i; // 错误!ri没有被初始化
}
// 正确形式:必须通过初始化列表的形式,显式地初始化引用和const函数
ConstRef::ConstRef(int ii) : i(ii), ci(ii), ri(i);
Ⅲ)构造函数初始值列表只说明用于初始化成员的值,而不限定初始化的具体执行顺序。成员的初始化顺序和它在类定义中出现的顺序一致,构造函数初始值列表中的初始值的前后位置关系不会影响实际的初始化顺序。最好令构造函数初始值的顺序和成员声明的顺序保持一致,可能的话,尽量避免使用某些成员初始化其他成员,而用构造函数的参数作为成员的初始值。
class X {
int i;
int j;
public:
X(int val):j(val), i(j) { } // 错误!i先被初始化,这里j未定义,不能给i赋值
// 可以改为:X(int val):j(val), i(val) { }
};
Ⅳ)如果一个构造函数为所有的参数都提供了默认实参,则它实际上也就定义了默认构造函数。
Ⅴ)C++11允许我们定义委托构造函数,使用它所属类的其他构造函数执行它自己的初始化过程,或者说把它自己的一些或全部职责委托给了其他构造函数。当一个构造函数委托给另一个构造函数时,受委托的构造函数的初始值列表和函数体被依次执行。
class Sales_data {
public:
// 非委托构造函数使用对应的实参初始化成员
Sales_data(std::string s, unsigned cnt, double price) : bookNo(s), units_sold(cnt), revenue(cnt * price) { }
// 其余构造函数全部委托给另一个构造函数
Sales_data() : Sales_data(" ", 0, 0) { }
Sales_data(std::string s) : Sales_data(s, 0, 0) { }
Sales_data(std::istream &is) : Sales_data() { read(is, *this); }
};
Ⅵ)隐式的类类型转换:如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称为转换构造函数。编译器只会主动地执行一步类型转换,下面的代码是错误的。
// 错误!需要用户定义的两种转换:1)把"9-999-99999-9"转换为string;2)再把string转换为Sales_data
item.combine("9-999-99999-9");
item.combine(string("9-999-99999-9")); // 正确:显式地转换为string,隐式地转换为Sales_data
item.combine(Sales_data("9-999-99999-9")); // 正确:隐式地转换为string,显式地转换为Sales_data
抑制构造函数定义的隐式转换,可以通过将构造函数声明为explicit加以阻止。
class Sales_data {
public:
Sales_data() = default;
Sales_data(const std::string &s, unsigned n, double p) : bookNo(s), units_sold(n), revenue(p * n) { }
explicit Sales_data(const std::string &s) : bookNo(s) { }
explicit Sales_data(std::istream&);
};
item.combine(null_book); // 错误!string构造函数是explicit的
item.combine(cin); // 错误!istream构造函数是explicit的
explicit Sales_data::Sales_data(istream& is) { // 错误!explicit 关键字只允许出现在类内的构造函数声明处
read(is, *this);
}
Sales_data item1(null_book); // 正确!直接初始化
Sales_data item2 = null_book; // 错误!不能将explicit 构造函数用于拷贝形式的初始化过程
Ⅶ)聚合类
当一个类满足如下条件时,我们说他是聚类的。
- 所有成员都是public的。
- 没有定义任何构造函数。
- 没有类内初始值。
- 没有基类,也没有virtual函数。
struct Data {
int ival;
string s;
};
可以提供一个花括号括起来的成员初始值列表,并用它初始化聚合类的数据成员。
// val1.ival = 0; val1.s = string("Anna")
Data val1 = {0, "Anna"};
// 错误!初始化顺序错误。
Data val1 = {"Anna", 1024};
7.6 类的静态成员
Ⅰ)声明静态成员:类的静态成员存在于任何对象之外,对象中不包含任何与静态数据成员有关的数据,静态成员函数不与任何函数绑定在一起,它们不包含this指针。作为结果,静态成员函数不能声明成const的,也不能在static函数体内使用this指针。
class Account {
public:
void calculate() { amount += amount * interestRate; }
static double rate() { return interestRate; }
static void rate(double);
private:
std::string owner;
double amount;
static double interestRate;
static double initRate();
};
Ⅱ)使用类的静态成员:使用作用域运算符直接访问静态成员。
double r;
r = Account::rate(); // 使用作用域运算符访问静态成员
Account ac1;
Account *ac2 = &ac1;
r = ac1.rate() // 通过Account的对象或引用
r = ac2 -> rate(); // 通过指向Account对象的指针
class Account {
// 成员函数不用通过作用域运算符就可以直接使用静态成员
public:
void calculate() { amount += amount * interestRate }
private:
static double interestRate;
}
Ⅲ)定义静态成员
1)既可以在类外也可以在类内定义静态成员函数,类似于全局变量,一旦定义将存在于程序运行的整个生命周期中。
2)当在外部定义时,也必须指明成员所属的类名,注意不能重复使用static关键字。静态成员不是由类的构造函数初始化的,必须在类的外部定义和初始化每个静态成员。
3)为了确保对象只定义一次,最好的办法是将静态数据成员的定义与其他非内联函数的定义放在同一个文件中。
Ⅳ)静态成员类内初始化
类的静态成员通常不应该在类的内部初始化,然而,我们可以为静态成员提供const整数类型的类内初始值,要求静态成员必须是字面值常量类型的constexpr。
class Account {
public:
static double rate() { return interestRate; }
static void rate(double);
private:
static constexpr int perid = 30;
double daily_tbl[period]; // period是常量表达式
};
Ⅴ)静态成员特殊场景
静态数据成员的类型可以就是它所属的类类型,而非静态成员则受到限制,只能声明成它所属类的指针或引用。并且,非静态成员不能作为默认实参,因为它本身属于类的一部分,无法真正提供一个对象以便从中获取成员的值,而静态成员可以作为默认实参。
class Bar {
public:
// ...
private:
static Bar mem1; // 正确:静态成员可以是不完全类型
Bar *mem2; // 正确:指针成员可以是不完全类型
Bar mem3; // 错误!数据成员必须是完全类型
};
class Screen {
public:
Screen& clear(char = bkground);
private:
static const char bkground;
};
第八章 IO库
前言
C++语言不直接处理输入输出,而是通过一族定义在标准库中的类型来处理IO。这些类型支持从设备读取数据、向设备写入数据的IO操作,设备可以是文件、控制台窗口等。还有一些类型允许内存IO,即从string读取数据,向string写入数据。
IO库定义了读写内置类型值的操作,此外,一些类如string通常也会定义类似的IO操作,来读写自己的对象。之前已经使用过的IO库设施小结:
istream | 输入流类型,提供输入操作 |
ostream | 输出流类型,提供输出操作 |
cin | 一个istream对象,从标准输入读取数据 |
cout | 一个ostream对象,向标准输出写入数据 |
cerr | 一个ostream对象,通常用于输出程序错误信息,写入到标准错误 |
>> | 用来从一个istream对象读取输入数据 |
<< | 用来向一个ostream对象写入输出数据 |
getline函数 | 从一个给定的istream对象读取一行数据,存入一个给定的string对象中 |
8.1 IO类
目前为止,我们使用的IO类型和对象都是操纵char数据的,并且默认情况下,这些对象都是关联到用户的控制台窗口的。而应用程序常常需读写命名文件,且处理string中的字符会很方便。因此,标准库还定义了其他一些IO类型。
头文件 | 类型 |
iostream | istream,wistream从流读取数据 |
ostream,wostream向流写入数据 | |
iostream,wiostream读写流 | |
fstream | ifstream,wifstream从文件读取数据 |
ofstream,wofstream向文件写入数据 | |
fstream,wfstream读写文件 | |
sstream | istringstream,wistringstream从string读取数据 |
ostringstream,wostringstream向string写入数据 | |
stringstream,wstringstream读写string |
注意:宽字符版本的类型和函数的名字以一个w开始,例如,wcin、wcout和wcerr。
Ⅰ)IO类型间的关系:由于类的继承机制,设备类型和字符大小不影响所执行的IO操作。例如,用 >> 读取数据,而不用考虑是从一个控制台窗口,一个磁盘文件还是一个string读取。
Ⅱ)IO对象无拷贝或赋值
ofstream out1, out2;
out1 = out2; // 错误!不能对流对象赋值
ofstream print(ofstream); // 错误!不能初始化ofstream参数
out2 = print(out2); // 错误!不能拷贝流对象
注意:不能拷贝IO对象,因此也不能将形参或返回类型设置为流类型。进行IO操作的函数通常以引用方式传递和返回流。读写一个IO对象会改变其状态,因此传递和返回的引用不能是const的。
Ⅲ)流的条件状态
strm::iostate | 提供了表达条件状态的完整功能 |
strm::badbit | 流已崩溃 |
strm::failbit | 指出一个IO操作失败了 |
strm::eofbit | 指出流到达了文件结束 |
strm::goodbit | 指出流未处于错误状态,此值保证为零 |
s.eof( ) | 若流s的eofbit置位,返回true |
s.fail( ) | 若流s的failbit或badbit置位,返回true |
s.bad( ) | 若流s的badbit置位,返回true |
s.good( ) | 若流s处于有效状态,返回true |
s.clear( ) | 将流s中所有条件状态位复位,将流的状态设置为有效,返回void |
s.clear(flags) | 根据给定的flags标志位,将流s中对应条件状态位复位,返回void |
s.setstate(flags) | 根据给定的flags标志位,将流s中对应条件状态位置位,返回void |
s.rdstate( ) | 返回流s的当前条件状态,返回值类型为strm::iostate |
在使用流之前,检查它是否处于良好状态。确定一个流对象的状态的最简单的方法是将它当作一个条件来使用。
int ival;
cin >> ival;
// 如果在标准输入上键入Boo,读操作就会失败。若输入文件结束标识,cin也会进入错误状态。
while (cin >> word)
// ok: 读取成功
查询流的状态:有时我们需要知道流为什么失败,IO库定义了一个与机器无关的iostate类型,提供了表达流状态的完整功能。使用eof、fail、bad和good函数来查询流当前的状态。
管理条件状态:使用clear、setstate和rdstate函数管理条件状态。
auto old_state = cin.rdstate(); // 记住cin的当前状态
cin.clear(); // 使cin有效
process_input(cin); // 使用cin
cin.setstate(old_state); // 将cin置为原有状态
Ⅳ)管理输出缓冲:刷新输出缓冲区可以使用endl(再输出一个换行)、flush和ends(再输出一个空字符),如果想在每次输出后都刷新缓冲区,可以使用unitbuf操纵符。当一个输入流被关联到一个输出流时,任何试图从输入流读取数据的操作都会先刷新关联的输出符。
cout << unitbuf; // 所有输出都刷新缓冲区
cout << nounitbuf; // 回到正常的缓冲方式
cin >> ival; // 标准库将cin和cout关联在一起,此语句也会导致cout的缓冲区被刷新
我们既可以将一个istream对象关联到另一个ostream,也可以将一个ostream关联到另一个ostream。
cin.tie(&cout); // old_tie 指向当前关联到cin的流(如果有的话)。这句仅仅用来展示:标准库已经默认将cin和cout关联在一起
ostream *old_tie = cin.tie(nullptr); // cin不再与其他流关联
cin.tie(&cerr); // 读取cin会刷新cerr,而不是cout。这不是一个好主意,因为cin应该关联到cout
cin.tie(old_tie); // 重建cin和cout间的正常关联
8.2 文件输入输出
除了继承自iostream类型的行为之外,fstream中定义的类型还增加了一些新的成员来管理与流相关的文件。
fstream fstrm; | 创建一个未绑定的文件流。fstream是头文件fstream中定义的一个类型 |
fstream fstrm(s); | 创建一个fstream,并打开名为s的文件。s可以是string类型,或者是一个指向C风格字符串的指针。 |
fstream fstrm(s, mode); | 和上一个相似,但按指定mode打开文件 |
fstrm.open(s) | 打开名为s的文件,并将文件与fstrm绑定 |
fstrm.close( ) | 关闭与fstrm绑定的文件,返回void |
fstrm.is_open( ) | 返回一个bool值,指出与fstrm关联的文件是否成功打开且尚未关闭 |
Ⅰ)使用文件流对象
当我们想要读写一个文件时,可以定义一个文件流对象,并将对象与文件关联起来。
ifstream in(ifile); // 构造一个ifstream并打开给定文件
ofstream out; // 定义了一个输出流out,输出文件流未关联到任何文件
// 这段代码定义了一个输入流in,它被初始化为从文件读取数据,文件名由string类型的参数ifile指定。
由于继承机制,我们可以用fstream代替iostream&
ifstream input(argv[1]); // 打开销售记录文件
ofstream output(argv[2]); // 打开输出文件
Sales_data total; // 保存销售总额的变量
if (read(input, total)) { // 读取第一条销售记录
Sales_data trans; // 保存下一条销售记录的变量
while (read(input, trans)) { // 读取剩余记录
if (total.isbn() == trans.isbn()) // 检查isbn
total.combine(trans); // 更新销售总额
else {
print(output, total); // 打印结果
total = trans; // 处理下一本书
}
}
print(output, total) << endl; // 打印最后一本书的销售额
} else
cerr << "No data?!" << endl; // 文件中无输入数据
Ⅱ)成员函数open和close
ifstream in(ifile); // 构筑一个ifstream并打开给定文件
ofstream out; // 输出文件流未与任何文件关联
out.open(ifile + ".copy"); // 打开指定文件
if (out) // 检查open是否成功,与之前用cin用作条件相似
in.close(); // 关闭文件
in.open(ifile + "2"); // 打开另一个文件
Ⅲ)自动构造和析构:当一个fstream对象被销毁时,close会被自动调用。
Ⅳ)文件模式:每个流都有一个关联的文件模式。
in | 以读方式打开 |
out | 以写方式打开(会丢弃已有数据) |
app | 每次写操作前均定位到文件末尾 |
ate | 打开文件后立即定位到文件末尾 |
trunc | 截断文件 |
binary | 以二进制方式进行IO |
8.3 string流
sstream strm; | strm是一个未绑定的stringstream对象 |
sstream strm(s); | strm是一个sstream对象,保存string s的一个拷贝 |
strm.str( ) | 返回strm所保存的string的拷贝 |
strm.str(s) | 将string s拷贝到strm中,返回void |
参考教程: C++ 文件和流
第九章 顺序容器
前言
一个容器就是一些特定类型对象的集合。顺序容器为程序员提供了控制元素存储和访问顺序的能力。这种顺序不依赖于元素的值,而是与元素加入容器时的位置相对应。标准库还提供了三种容器适配器,分别为容器操作定义了不同的接口,来与容器类型匹配。
9.1 顺序容器概述
vector | 可变大小数组。支持快速随机访问。在尾部之外的位置插入/删除元素很慢 |
deque | 双端队列(double-ended queue)。支持快速随机访问。在头尾位置插入或删除元素很快 |
list | 双向链表。只支持双向顺序访问。在list中任何位置进行插入/删除操作速度都很快 |
forward_list | 单向链表。只支持单向顺序访问。在链表任何位置进行插入/删除操作速度都很快 |
array | 固定大小数组。支持快速随机访问。不能添加或删除元素 |
string | 与vector相似的容器,但专门用于保存字符。随机访问快。在尾部插入/删除速度快 |
Ⅰ)除了固定大小的array外,其他容器都提供高效、灵活的内存管理,可以添加和删除元素,扩张和收缩容器的大小。容器保存元素的策略对容器操作的效率有着固定的,有时是重大的影响。
1)string和vector将元素保存在连续的内存空间中,元素的下标来计算其地址是非常快速的,但是在两种容器的中间位置添加或删除元素非常耗时。
2)list和forward_list两个容器的设计目的是令容器任何位置的添加和删除操作都很快速。作为代价,这两个容器不支持元素的随机访问。与vector、deque和array相比,这两个容器的额外内存开销也很大。
3)deque是一个更为复杂的数据结构,元素可以从两端弹出。与string和vector类似,deque支持快速随机访问。与string和vector一样,在两种容器的中间位置添加或删除元素的代价(可能)很高。但是在deque的两端添加或删除元素很快,与list或forward_list添加/删除元素的速度相当。
4)forward_list和array是新C++标准增加的类型。与内置数组相比,array更加安全、更容易使用。forward_list的设计目标是达到与最好的手写的单向链表数据结构相当的性能。因此,forward_list没有size操作,因为保存或计算其大小就会比手写链表多出额外的开销。对其他容器而言,size保证是一个快速的常量时间的操作。
注意:现代C++程序应该使用标准库容器,而不是更原始的数据结构。
Ⅱ)选择容器的基本原则
1)除非有很好的理由选择其他容器,否则使用vector是最好的选择。
2)如果程序有很多小元素且空间的额外开销很重要,不要使用list或forward_list。
3)要求随机访问元素,应该使用vector或deque。
4)要求中间插入或删除元素,应该使用list或forward_list。
5)要求在头尾插入或删除元素,且中间不进行插入或删除,应该使用deque。
6)如果只在读取输入时才需要在容器中间位置插入元素,随后需要随机访问元素。首先可以考虑在读取输入时使用vector,再调用sort函数重排容器中的元素,从而避免在中间位置添加元素。如果必须在中间位置插入元素,考虑在输入阶段使用list,输入完成将list拷贝到vector中。
注意:如果实在不确定使用哪种容器,可以在程序中只使用vector和list的公共操作迭代器而非下标,避免随机访问。这样可以在必要时选择使用vector或list。
9.2 容器库概览
本节将介绍所有的容器都支持的操作。一般说来,每个容器都定义在一个头文件中,文件名和类型名相同,容器均定义为模板类。我们可以定义一个保存这种类型对象的容器,但我们在构造这种容器时不能只传递给它一个元素数目参数。
vector<noDefault> v1(10, init); // 正确:提供了元素初始化器
vector<noDefault> v2(10); // 错误!必须提供一个元素初始化器
类型别名 | |
iterator | 此容器类型的迭代器类型 |
const_iterator | 可以读取元素,但不能修改元素的迭代器类型 |
size_type | 无符号整数类型,足够保存此种容器类型最大可能容器的大小 |
different_type | 带符号整数类型,足够保存两个迭代器之间的距离 |
value_type | 元素类型 |
reference | 元素的左值类型,与value_type&含义相同 |
const_reference | 元素的const左值类型,即constvalue_type& |
构造函数 | |
C c; | 默认构造函数,构造空容器 |
C c1(c2); | 构造 c2 的拷贝 c1 |
C c(b, e) | 构造 c,将迭代器 b 和 e 指定的范围内的元素拷贝到 c(array不支持) |
C c{a, b, c...} | 列表初始化 c |
赋值与swap | |
c1 = c2; | 将c1中的元素替换为c2中元素 |
c1 = {a, b, c...} | 将c1中的元素替换为列表中元素(不适用于array) |
a.swap(b) | 交换a和b的元素 |
swap(a, b) | 与a.swap(b)等价 |
大小 | |
c.size( ) | c中元素的数目(不支持forward_list) |
c.max_size( ) | c可保存的最大元素数目 |
c.empty( ) | 若c中存储了元素,返回false,否则返回true |
添加/删除元素(不适用于array) 注意:在不同容器中,这些操作的接口都不同 | |
c.insert(args) | 将args中的元素拷贝进c |
c.emplace(inits) | 使用inits构造c中的一个元素 |
c.erase(args) | 删除args指定的元素 |
c.clear( ) | 删除c中所有的元素,返回void |
关系运算符 | |
==, != | 所有的容器都支持相等运算符 |
<, <=, >, >= | 关系运算符 |
获取迭代器 | |
c.begin( ), c.end( ) | 返回指向c的首元素和尾元素之后位置的迭代器 |
c.cbegin( ), c.cend( ) | 返回const_iterator |
反向容器的额外成员(不支持forward_list) | |
reverse_iterator | 按逆序寻址元素的迭代器 |
const_reverse_iterator | 不能修改元素的逆序迭代器 |
c.rbegin( ), c.rend( ) | 返回指向c的尾元素和首元素之前位置的迭代器 |
c.crbegin( ), c.crend( ) | 返回const_reverse_iterator |
Ⅰ)迭代器范围
1)一个迭代器范围由一对迭代器表示,两个迭代器分别指向同一个容器中的元素或者是尾元素之后的位置。这两个迭代器通常被称为begin和end,或者是first和last,标记了容器中元素的一个范围,左闭合区间[begin, end)。
2)对构成范围迭代器的要求(编译器不会强制,但是程序员应该满足)
- 它们指向同一个容器中的元素,或者是容器最后一个元素之后的位置。
- 可以通过递增begin来到达end(即end不在begin之前)
while (begin != end) {
*begin = val; // 正确:范围非空,因此begin指向一个元素
++begin; // 移动迭代器,获取下一个元素
}
auto it7 = a.begin(); // 仅当a是const时,it7是const_iterator
auto it8 = a.cbegin(); // it8是const_iterator
Ⅱ)容器类型成员
反向迭代器:反向遍历容器的迭代器,与正向迭代器相比,各种操作的含义发生了颠倒。如对一个反向迭代器执行++操作,会得到上一个元素。
Ⅲ)容器定义和初始化
当将一个容器初始化为另一个容器的拷贝时,两个容器的容器类型和元素类型都必须相同。不过,当传递迭代器参数来拷贝一个范围时,就不要求容器类型是相同的了。并且,新容器和原容器中的元素类型也可以不同,只要能将拷贝的元素转换为要初始化的容器的元素类型即可。
// 将一个容器初始化为另一个容器的拷贝
list<string> authors = {"Milton", "Shakespeare", "Austen"}; // 列表初始化
vector<const char*> articles = {"a", "an", "the"};
list<string> list2(authors); // 正确:类型匹配
deque<string> authList(authors); // 错误!容器类型不匹配
vector<string> words(articles); // 错误!容器类型不匹配
forward_list<string> words(articles.begin(), articles.end()); // 正确,可以将const char*转换为string
deque<string> authList(authors.begin(), it); // 拷贝元素,直到 it-1 指向的元素
// 与顺序容器大小相关的构造函数
vector<int> ivec(10, -1); // 10个int元素每个都是-1
list<string> svec(10, "hi!"); // 10个string元素每个都是“hi!”
forward_list<int> ivec(10); // 10个int元素每个都是0
deque<string> svec(10); // 10个string元素每个都空字符串
注意:1)只有顺序容器的构造函数才接受大小参数,关联容器不支持。 2)标准库array具有固定大小。
array<int, 42> // 保存42个int的数组
array<string, 10> // 保存10个string的数组
array<int, 10>::size_type i;
array<int>::size_type j; // 错误!array<int>不是一个类型
array<int, 10> ial;
array<int, 10> ia2 = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
array<int, 10> ia3 = {42};
// 对内置数组类型不能进行拷贝或对象赋值操作
int digs[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int cpy[10] = digs;
// 对array可以进行拷贝或对象赋值操作
array<int, 10> digits = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
array<int, 10> copy = digits;
array<int, 10> a1 = {0}; // 正确
a1 = {0}; // 错误!不能将一个花括号列表赋予数组
9.3 顺序容器操作
Ⅰ)添加元素
这些操作会改变容器大小:array不支持这些操作 forward_list有自己专门版本的insert和emplace:不支持push_back和emplace_back vector和string不支持push_front和emplace_front | |
c.push_back(t) c.emplace_back(args) | 在c的尾部创建一个值为t或由args创建的元素,返回void |
c.push_front(t) c.emplace_front(args) | 在c的头部创建一个值为t或由args创建的元素,返回void |
c.insert(p, t) c.emplace(p, args) | 在迭代器p指向的元素之前创建一个值为t或由args创建的元素,返回指向新添加的元素的迭代器 |
c.insert(p, n, t) | 在迭代器p指向的元素之前插入n个值为t的元素,在迭代器p指向的元素之前创建一个值为t或由args创建的元素,返回指向新添加的元素的迭代器;若n为0,则返回p |
c.insert(p, b, e) | 将迭代器b和e指定的范围内的元素插入到迭代器p指向的元素之前。b和e不能指向c中的元素,返回指向新添加的第一个元素的迭代器,若范围为空,则返回p |
c.insert(p, il) | il是一个花括号包围的元素值列表,将这些给定值插入到迭代器p指向的元素之前,返回指向新添加的第一个元素的迭代器,若列表为空,则返回p |
注意:向一个vector、string或deque插入元素会使所有指向容器的迭代器、引用和指针失效。
使用push_back(array和front_list不支持)
// 从标准容器读取数据,将每个单词放到容器末尾
string word;
while (cin >> word)
container.push_back(word);
// 在string末尾添加字符
void pluralize(size_t cnt, string &word) {
if (cnt > 1)
word.push_back('s');
}
使用push_front
// 将0,1,2,3添加到列表头部
list<int> ilist;
for (size_t ix = 0; ix != 4; ++ix)
ilist.push_front(ix);
特定位置添加元素
insert成员提供了更为一般的添加功能,它允许我们在容器中任意位置插入0个或多个元素,将元素插入到迭代器所指定的位置之前。
slist.insert(iter, "Hello!"); // 将"Hello!"添加到iter之前的位置
vector<string> svec;
list<string> slist;
slist.insert(slist.begin(), "Hello!"); // 等价于调用slist.push_front("Hello!")
svec.insert(svec.begin(), "Hello!"); // vector不支持push_front,但我们可以插入到begin()之前,插入到vector末尾之外的任何位置都可能很慢
插入范围内元素
// 接受一个元素数目和一个值将指定数量的元素添加到指定位置之前
svec.insert(sec.end(), 10, "Anna");
// 接受一对迭代器或一个初始化列表的insert版本将给定范围内的元素插入到指定位置之前
vector<string> v = {"quasi", "simba", "frollo", "scar"};
slist.insert(slist.begin(), v.end() - 2, v.end());
slist.insert(slist.end(), {"there", "words", "will", "go", "at", "the", "end"});
// 运行时错误:迭代器表示要拷贝的范围,不能指向与目的位置相同的容器。
slist.insert(slist.begin(), slist.begin(), slist.end());
// 使用insert的返回值,在容器中的一个特定位置反复插入元素(重要!!!)
list<string> lst;
auto iter = lst.begin();
while (cin >> word)
iter = lst.insert(iter, word); // 等价于调用push_front
使用emplace操作
Ⅱ)访问元素
// 在解引用一个迭代器或调用front或back之前检查是否有元素
if (!c.empty()) {
auto val = *c.begin(), val2 = c.front(); // val1和val2是c中第一个元素值的拷贝
auto last = c.end; // val3和val4是c中最后一个元素值的拷贝
auto val3 = *(--last); // 不能递减forward_list迭代器
auto val4 = c.back(); // forward_list不支持
}
// front和back两个操作分别返回首元素和尾元素的引用。
at和下标操作只适用于string、vector、deque和array back不适用于forward_list | |
c.back( ) | 返回c中尾元素的引用。若c为空,函数行为未定义 |
c.front( ) | 返回c中首元素的引用。若c为空,函数行为未定义 |
c[n] | 返回c中下标为n的元素的引用,n是一个无符号整数。n>=c.size(),则函数行为未定义 |
c.at[n] | 返回下标为n的元素的引用,如果下标越界,则抛出一个out_of_range异常 |
Ⅲ)删除元素
这些操作会改变容器的大小,所以不适用于array。 forward_list有特殊的版本erase,不支持pop_back;vector和string不支持pop_front | |
c.pop_back( ) | 删除c中尾元素。若c为空,则函数行为未定义,返回void |
c.pop_front( ) | 删除c中首元素。若c为空,则函数行为未定义,返回void |
c.erase(p) | 删除迭代器p所指的元素,返回一个指向被删元素之后元素的迭代器,若p指向尾元素,则返回尾后迭代器。若p是尾后迭代器,则函数行为未定义。 |
c.erase(b,e) | 删除迭代器b和e所指范围内的元素,返回一个指向最后一个被删除元素之后元素的迭代器,若e本身就是尾后迭代器,则函数也返回尾后迭代器。 |
c.clear( ) | 删除c中所有的元素,返回void。 |
Ⅳ)特殊的forward_list操作
Ⅴ)改变容器大小
list<int> ilist(10, 42); // 10个int,每个值都是42
ilist.resize(15); // 将5个0添加到ilist尾部
ilist.resize(25, -1); // 将10个值为-1的元素添加到ilist的尾部
ilist.resize(5); // 从ilist末尾删除20个元素
Ⅵ)容器操作可能使迭代器失效
向容器中添加元素 | |
vector或string | ① 存储空间被重新分配,迭代器、指针和引用失效。② 存储空间未被重新分配,插入位置之前的元素的迭代器、指针和引用仍有效,之后的失效。 |
deque | ① 插入到除首尾位置之外的任何位置都会使迭代器、指针和引用失效。② 插入到首尾位置,迭代器失效,指向存在的元素的指针和引用不失效。 |
list和forward_list | 指向容器的迭代器、指针和引用仍有效 |
从容器中删除元素 | |
vector或string | 指向被删元素之前元素的迭代器、指针和引用仍有效,之后的失效 |
deque | ① 删除除首尾位置之外的任何位置都会使迭代器、指针和引用失效。② 删除尾元素,尾后迭代器失效,其他迭代器、指针和引用不受影响。 |
list和forward_list | 指向容器的迭代器、指针和引用仍有效。删除元素时,尾后迭代器总是会失效。 |
向容器中添加元素和从容器中删除元素的操作可能会使指向容器元素的迭代器、指针和引用失效。 因此,必须保证每次改变容器的操作之后都正确地重新定位迭代器。
// 删除偶数元素,复制奇数元素
vector<int> vi = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
auto iter = vi.begin(); // 调用begin()而不是cbegin(),因为要改变vi
while (iter != vi.end()) {
if (*iter % 2) {
iter = vi.insert(iter, *iter); // 复制当前元素
iter += 2; // 向前移动迭代器,跳过当前元素以及插入到它之前的元素
} else
iter = vi.erase(iter); // 删除偶数元素
} // 不应向前移动迭代器,iter指向我们删除的元素之后的元素
注意:添加或删除deque、string或vector中的元素的循环程序必须反复调用end,而不能在循环之前保存end返回的迭代器。
while (begin != v.end()) {
++begin;
begin = v.insert(begin, 42);
++begin;
}
9.4 vector对象是如何增长的
shrink_to_fit只适用于vector、string和deque;capacity和reserve只适用于vector和string | |
c.shrink_to_fit() | 将capacity()减少为与size()相同大小 |
c.capacity() | 不重新分配内存空间的话,c可以保存多少元素 |
c.reserve(n) | 分配至少能容纳n个元素的内存空间。不改变容器中元素的数量,仅影响vector预先分配多大的内存空间 |
9.5 额外的string操作
n、len2和pos2都是无符号值 | |
string s(cp, n) | s是cp指向数组中的前n个字符的拷贝,此数组至少应该包含n个字符 |
string s(s2, pos2) | s是string s2从下标pos2开始的字符的拷贝,若pos2 > s2.size(),构造函数的行为未定义 |
string s(s2, pos2, len2) | s是string s2从下标pos2开始len2个字符的拷贝,若pos2 > s2.size(),构造函数的行为未定义。不管len2的值是多少,构造函数最多拷贝s2.size()-pos2个字符 |
s.substr(pos, n)操作:返回一个string,包含s中从pos开始的n个字符的拷贝。pos的默认值为0,n的默认值为s.size()-pos,即拷贝从pos开始的所有字符。
注意:由于string相关操作比较多,这里不一一列举了,在要用的时候在本书查找相关操作即可。
9.6 容器适配器
除了顺序容器外,标准库还定义了三个顺序容器适配器:stack、queue和priority_queue。
Ⅰ)定义一个适配器:
stack<int> stk(deq); // 从deq拷贝元素到stk
stack<string, vector<string>> str_stk; // 在vector上实现的空栈
stack<string, vector<string>> str_stk2(svec); // str_stk2在vector上实现,初始化时保存svec的拷贝
Ⅱ)栈适配器:
stack<int> intStack; // 声明语句定义了一个保存整型元素的栈intStack
// 填满栈
for (size_t ix = 0; ix != 10; ++ix)
intStack.push(ix); // intStack保存0到9十个数
while (!intStack.empty()) { // intStack中有值就继续循环
int value = intStack.top; // 使用栈顶值
intStack.pop(); // 弹出栈顶元素,继续循环
}
Ⅲ)队列适配器:queue和priority_queue适配器定义在queue头文件中。
参考教程: C++顺序容器知识总结
第十章 泛型算法
前言
用户可能还希望对容器做很多其他操作,标准库并没有给每个容器定义成员函数来实现这些操作,而是定义了一组泛型算法。泛型是指它们可以用于不同类型的元素和多种容器类型,以及其他类型序列。
10.1 概述
一般情况下,这些算法不能直接操作容器,而是遍历由两个迭代器指定的一个元素范围来进行操作。例如对于算法find的使用:
int val = 42; // 调用标准库算法find:在vec中查找想要的元素
auto result = find(vec.c.begin(), vec.end(), val);
cout << "The value " << val << (result == vec.cend() ? "is not present" : "is present") << endl;
// 调用标准库算法find:在一个string的list中查找一个给定值
string val = "a value";
auto result = find(lst.cbegin(), lst.cend(), val); // 用find在数组中查找值
int ia[] = {27, 210, 12, 47, 109, 83};
int val;
int* result = find(begin(ia), end(ia), val);
auto result = find(ia + 1, ia + 4, val); // 在序列的子范围中查找,只需要将指向子范围首元素和尾元素之后位置的迭代器(指针)传递给find
注意:迭代器令算法不依赖于容器,但算法依赖于元素类型的操作。泛型算法本身不会执行容器的操作,它们只会运行于迭代器之上,执行迭代器的操作。因此,算法永远不会改变容器的大小,只可能改变容器中保存的元素的值或顺序。
10.2 初识泛型算法
Ⅰ)只读算法:只会读取其输入范围内的元素,而从不改变元素。
accumulate:求和算法。
int sum = accumulate(vec.cbegin(), vec.cend(), 0);
// 对vec中的元素求值,和的初值是0。第三个参数的类型决定了函数中使用哪个加法运算以及返回值的类型。
string sum = accumulate(v.cbegin(), v.cend(), string("")); // 将vector中所有string元素连接
string sum = accumulate(v.cbegin(), v.cend(), ""); // 错误,传递的是字符串字面值,用于保存和的对象的类型将是const char*,没有定义+运算符。
equal:用于确定两个序列是否保存相同的值,相同返回true,否则返回false。
equal(roster1.cbegin(), roster1.cend(), roster2.cbegin());
// 将第一个序列中的每个元素与第二个序列中的对应元素比较,元素类型不是必须一样,如可以是vector<string>和list<const char*>
// 接受3个迭代器,前两个接受第一个序列中的元素范围,第三个表示第二个序列的首元素。
注意:对于只读取不改变元素的算法,通常最好使用cbegin()和cend()。但是,如果使用算法返回的迭代器改变元素的值,就需要使用begin()和end()。
Ⅱ)写容器元素的算法
fill(vec.begin(), vec.end(), 0); // 将每个元素重置为0
fill(vec.begin(), vec.begin() + vec.size/2, 10); // 将容器的一个子序列设置为10
// 迭代器表示范围,第三个参数用于接受一个值赋予输入序列中的每个元素
注意:① 序列原大小至少不小于要求算法写入的元素数目。② 操作两个序列的算法之间的区别在于我们如何传递第二个序列。③ 用一个单一迭代器表示第二个序列的算法都假定第二个序列至少与第一个一样长。
// 用fill_n将一个新值赋予vector中的元素
vector<int> ivec; // 空vector
fill_n(ivec.begin(), ivec.size(), 0); // 所有元素重置为0,调用形式:fill_n(dest, n, val);
// 注意:假定dest指向一个元素,dest开始的序列至少包含n个元素
vector<int> ivec; // 空vector
fill_n(ivec.begin(), 10, 0); // 错误!想要写入10个元素,但ivec中实际没有元素,语句未定义
插入迭代器 back_inserter:接受一个指向容器的引用,返回一个与该容器绑定的插入迭代器,我们可以通过此迭代器赋值,将一个具有给定值的元素添加到容器中。
vector<int> ivec; // 空向量
auto it = back_inserter(ivec); // 将元素添加到ivec中
*it = 42; // ivec中现在有一个元素:42
vector<int> vec; // 空向量
fill_n(back_inserter(vec), 10, 0); // 使用back_inserter创建一个迭代器,作为算法的目的位置使用,向vec的末尾添加了10个元素0。
拷贝算法:将输入范围内的元素拷贝到目的序列中。
// 使用copy实现内置数组的拷贝
int a1[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int a2[sizeof(a1)/sizeof(*a1)]; // 确保a2和a1大小一样
auto ret = copy(begin(a1), end(a1), a2); // ret指向拷贝到a2的尾元素之后的位置
// replace读入一个序列,并将其中所有等于给定值的元素都改为另一个值
replace(ilst.begin(), ilst.end(), 0, 42) // 将所有值为0的元素改为42
// ilst并未改变,ivec包含ilst的一份拷贝,不过原来在ilst中的值为0的元素在ivec中都变成42
replace_copy(ilst.cbegin(), ilst.cend(), back_inserter(ivec), 0, 42);
Ⅲ)重排容器元素的算法
void elimDups(vector<string> &words) {
sort(words.begin(), words.end());
// 按字典序排序
auto end_unique = unique(words.begin(), words.end());
// unique重排输入范围,使得每个单词只出现一次
// 将出现一次的单词排列在范围的前部,返回指向不重复区域之后一个位置的迭代器
words.erase(end_unique, words.end());
// 删除重复元素,end_unique到容器尾部保存重复的单词。注意:即使没有重复元素也可以。
}
注意:标准库算法对迭代器而不是容器进行操作。因此,算法不能添加或删除元素。
10.3 定制操作
Ⅰ)向算法传递函数
谓词:谓词是一个可调用的表达式,其返回结果是一个能用做条件的值。标准库算法所使用的谓词有一元谓词(只接受单一参数)和二元谓词(它们有两个参数)。接受谓语参数的算法对输入序列中的元素调用谓语。因此,元素类型必须能转换为谓语的参数类型。
bool isShorter(const string &s1, const string &s2) {
return s1.size() < s2.size();
}
// 按长度由短至长排序words
short(words.begin(), words.end(), isShorter);
elimDups(words); // 将words按字典序排序,并消除重复单词。
// 按长度重新排序,长度相同的单词维持字典序
stable_sort(words.begin(), words.end(), isShorter);
for (const auto &s : words)
cout << s << " ";
cout << endl;
Ⅱ)lambda表达式
可调用对象:对于一个对象或一个表达式,如果可以对其使用调用运算符,则称它是可调用的。可调用对象有:函数和函数指针、重载了函数调用运算符的类以及lambda表达式。
一个lambda表达式表示一个可调用的代码单元,可以将它理解为一个未命名的内联函数。与任何函数相似,一个lambda表达式具有一个返回类型、一个参数列表和一个函数体。但是,与函数不同,可能定义在函数内部。lambda表达式的形式:
[capture list] (parameter list) -> return type {function body}
其中,capture list 是一个lambda所在函数中定义的局部变量的捕获列表(局部变量列表,通常为空),与普通函数不同,lambda必须使用尾置返回来指定返回类型。
auto f = [] { return 42; };
cout << f() << endl;
/*
* 可以忽略参数列表和返回类型,但是必须永远包含捕获列表和函数体。
* 调用方式和普通函数的调用方式相同。
*/
向lambda传递参数:lambda不能有默认参数。
#include<iostream>
using namespace std;
void biggies(vector<string> &words, vector<string>::size_type sz) {
elimDups(words); // 将words按字典序排序,删除重复单词
stable_sort(words.begin(), words.end(), [](const string &a, const string &b) { return a.size() < b.size(); }); // 按长度排序,当stable_sort需要比较两个元素时,它就会调用给定的这个lambda表达式。
auto wc = find_if(words.begin(), words.end(), [sz](const string &a) { return a.size() >= sz; }) // 利用捕获列表将sz给lambda表达式。获取一个迭代器,指向第一个满足size() >= sz的元素
auto count = words.end() - wc; //计算满足size >= sz 的元素的数目
cout << count << " " << make_plural(count, "word", "s") << "of length" << sz << "or longer" << endl;
for_each(wc, words.end(), [](const string &s) { cout << s << " "; });
cout << endl;
}
int main() {
vector<string> words = {"the", "quick", "red", "fox", "jumps", "over", "the", "slow", "red", "turtle"};
biggies(words, 4);
}
使用捕获列表(局部变量列表): 向函数传递lambda时,同时定义了一个未命名的新类型和该类型的一个对象。默认情况下,新类型包括了捕获的变量,作为数据成员。
// 值捕获
void fcn() {
size_t v1 = 42; // 局部变量
auto f = [v1]{ return v1; } // 将v1的值拷贝到名为f的可调用对象
v1 = 0;
auto j = f(); // j为42,f在上面存储了v1的值
}
// 引用捕获
void fcn2() {
size_t v1 = 42;
auto f2 = [&v1]{ return v1; } // f2是对v1的引用,不对值进行存储。注意:必须保证在lambda表达式执行时v1已经存在。
v1 = 0;
auto j = f2(); // j的值为v1的最新值
}
// 隐式捕获:让编译器根据lambda体中的代码来判断需要使用哪些变量。&:隐式引用捕获;=:隐式拷贝捕获。
wc = find_if(words.begin(), words.end(), [=](const string &s) { return s.size() >= sz; })
// 混合使用隐式捕获和显式捕获
void biggies(vector<string> &words, vector<string>::size_type sz, ostream &os = out, char c = ' ') {
for_each(words.begin(), words.end(), [&, c](const string &s) { os << s << c; });
for_each(words.begin(), words.end(), [=, &os](const string &s) { os << s << c; });
}
// 可变 lambda
void fcn3() {
size_t v1 = 42;
auto f3 = [v1]() mutable { return ++v1; } // 对于值拷贝的变量,如果函数体里面需要修改它的值,必须加上关键字mutable。先拷贝,再改变。
v1 = 0;
auto j = f3(); // j的值为v1在函数体内的最新值 43
}
void fcn4() {
size_t v1 = 42;
auto f4 = [v1]{ return ++v1; }
v1 = 0;
auto j = f4(); // 对于非const变量的引用,可以通过f4中的引用修改,上一条语句v1=0,这里加一变为1
}
Ⅳ)参数绑定
- 标准库bind函数:定义在头文件functional中,接受一个可调用对象,生成一个新的可调用对象来“适应”原对象的参数列表:
auto newCallable = bind(callable, arg_list)
newCallable本身是一个可调用对象,arg_list是一个逗号分隔的参数列表,对应给定的callable的参数。当我们调用newCallable时,newCallable会调用callable,并传递给它arg_list中的参数。
arg_list中的参数可以包含形如_n的名字,其中n是一个整数。这些参数是“占位符”,表示newCallable的参数,它们占据了传递给newCallable的参数的位置。
using namespace std;
using namespace std::placeholders;
vector<string> words = {"string1", "abcde"}
bool check_size(const string&s, string::size_type sz) {
return s.size() >= sz;
}
int main() {
auto check6 = bind(check_size, _1, 6); // 只有一个占位符,表示check6只接受单一参数。
string s = "hello";
bool b1 = check6(s); // check6(s)会调用check_size(s, 6)
auto wc = find_if(words.begin(), words.end(), bind(check_size, _1, 6));
auto wc2 = find_if(words.begin(), words.end(), check6);
}
- bind的参数
auto g = bind(f, a, b, _2, c, _1);
// g是一个有两个参数的可调用对象。g(X, Y)的调用会映射到f(a, b, Y, c, X)
- 用bind重排参数顺序
sort(words.begin(), words.end(), isShorter); // 单词由长至短排序
sort(words.begin(), words.end(), bind(isShorter, _2, _1)); // 单词由短至长排序
- 绑定引用参数
for_each(words.begin(), words.end(), bind(print, os, _1, ' '));
for_each(words.begin(), words.end(), bind(print, ref(os), _1, ' '));
// 对于ostream对象,不能拷贝。
10.4 再探迭代器
Ⅰ)插入迭代器
- back_inserter:创建一个使用push_back的迭代器。
- front_inserter:创建一个使用push_front的迭代器。
- inserter:创建一个使用insert的迭代器。
// it是由insert生成的迭代器
*it = val;
// 这两条代码效果一样
it = c.insert(it, val); // it指向新插入的元素
++it; // 递增it使得它指向原来的元素
list<int> lst = {1, 2, 3, 4};
list<int> lst2, lst3;
copy(lst.cbegin(), lst.cend(), front_inserter(lst2)); // lst2包含4,3,2,1
copy(lst.cbegin(), lst.cend(), inserter(lst3, lst3.begin)); // lst3包含1,2,3,4
Ⅱ)iostream迭代器
istream_iterator<int> int_iter(cin); // 绑定一个流,从cin读取int
istream_iterator<int> eof; // 默认初始化迭代器,尾后迭代器
while(in_iter != eof)
vec.push_back(*in_inter++); // 解引用迭代器获得从流读取的前一个值
// 可以将程序重写为下面的形式
istream_iterator<int> int_iter(cin), eof;
vector<inter> vec(in_iter, eof); // 从迭代器范围构造vec
ifstream in("afile");
istream_iterator<string> str_it(in); // 从“afile”读取字符串
使用算法操作流迭代器:
istream_iterator<int> in(cin), eof;
cout << accumulate(in, eof, 0) << endl; // 计算从标准输入读取的值的和,如果输入为1 2 5,输出为8
Ⅲ)反向迭代器:反向迭代器需要递减运算符。
10.5 泛型算法结构
Ⅰ)5类迭代器
类别 | 特点 | 功能 | 使用场合 |
输入迭代器 | 只读,不写,单遍扫描,只能递增 | 支持== != ++ * ->操作 | find、accumulate |
输出迭代器 | 只写,不读,单遍扫描,只能递增 | 支持++ *操作 | copy |
前向迭代器 | 可读写,多遍扫描,只能递增 | 支持所有输入输出迭代器操作 | replace,forword_list |
双向迭代器 | 可读写,多遍扫描,可递增递减 | 支持所有输入输出迭代器操作和 -- | reverse |
随机访问迭代器 | 可读写,多遍扫描,支持全部迭代器运算 | 支持双向迭代器的所有功能和关系运算符、加减运算、减法运算和下标运算 | 算法sort、容器array、deque、string和vector |
Ⅱ)算法参数模型
大多数算法具有如下4种形式之一:
alg(beg, end, other args);
alg(beg, end, dest, other args);
alg(beg, end, beg2, other args);
alg(beg, end, beg2, end2, other args);
// alg表示算法名,beg和end表示算法所操作的输入范围
// dest、beg2、 end2都是迭代器参数,dest参数是一个表示算法可以写入的目的位置的迭代器
注意:向输出迭代器写入数据的算法都假定目标空间足够容纳写入的数据。
Ⅲ)算法命名规范
- 一些算法使用重载形式传递一个谓词:
unique(beg, end); // 使用==运算符比较元素 unique(beg, end, comp); // 使用comp比较元素
- _if版本的算法:
find(beg, end, val); // 查找输入范围中val第一次出现的位置 find_if(beg, end, pred); // 查找第一个令pred为真的元素
- 拷贝元素和不拷贝元素:
reverse(beg, end); // 反转输入范围中元素的顺序 reverse_copy(beg, end, dest); // 将元素逆序拷贝到dest remove_if(v1.begin(), v1.end(), [](int){ return i % 2; }); // 从v1中删除奇数元素 remove_copy_if(v1.begin(), v1.end(), back_inserter(v2), [](int){ return i % 2; }); // 将偶数元素从v1拷贝到v2,v1不变
10.6 特定容器算法
注意:对于list和forword_list,应该优先使用成员函数版本的算法,而不是通用算法,因为代价太大。
lst.merge(lst2) | 将来自lst2的元素合并入lst,lst和lst2必须都是有序的 |
lst.merge(lst2, comp) | 元素将从lst2中删除。在合并之后,lst2变为空。第一个版本使用<运算符;第二个版本使用给定的比较操作 |
lst.remove(val) | 调用erase删除掉与给定值相等或令一元谓词为真的每个元素 |
lst.remove_if(pred) | |
lst.reverse( ) | 反转lst中元素的顺序 |
lst.ort( ) | 使用<或给定比较操作排序元素 |
lst.sort(comp) | |
lst.unique( ) | 调用erase删除同一个值的连续拷贝。第一个版本使用==;第二个版本使用给定的二元谓词。 |
lst.unique(pred) |
链表还定义了splice算法,是链表数据结构所特有的,因此不需要通用版本。
注意:链表特有的操作会改变容器。
参考教程:C++模板与标准模板库
第十一章 关联容器
前言
按关键字有序保存元素 | |
map | 关联数组;保存关键字-值对 |
set | 关键字即值,即只保存关键字的容器 |
multimap | 关键字可重复出现的map |
multiset | 关键字可重复出现的set |
无序集合 | |
unordered_map | 用哈希函数组织的map |
unordered_set | 用哈希函数组织的set |
unordered_multimap | 哈希函数组织的map,关键字可重复出现 |
unordered_multiset | 哈希函数组织的set,关键字可重复出现 |
11.1 使用关联容器
// 单词计数程序
map<string, size_t> word_count; // string到size_t的空map,关键字是string类型,值是size_t类型。
string word;
while (cin >> word)
++word_count[word]; // 提取word的计数器并将其加一
for (const auto &w : word_count)
cout << w.first << "occurs" << w.second << ((w.second > 1) ? "times" : "time") << endl;
// 单词计数程序,排除部分单词
map<string, size_t> word_count; // string到size_t的空map,关键字是string类型,值是size_t类型。
set<string> exclude = {"The", "But", "And", "Or", "An", "A",
"the", "but", "and", "or", "an", "a"};
string word;
while (cin >> word)
if (exclude.find(word) == exclude.end()) // 检查单词是否在忽略集中,find返回一个迭代器,在set中指向该关键字,否则返回尾后迭代器。这里,排除在exclude里面的单词。
++word_count[word];
for (const auto &w : word_count)
cout << w.first << "occurs" << w.second << ((w.second > 1) ? "times" : "time") << endl;
11.2 关联容器概述
Ⅰ)定义关联容器
定义一个map时,必须既指明关键字类型又指明值类型;而定义一个set时,只需指明关键字类型,因为set中没有值。
map<string, size_t> word_count; // string到size_t的空map,关键字是string类型,值是size_t类型。
set<string> exclude = {"The", "But", "And", "Or", "An", "A",
"the", "but", "and", "or", "an", "a"};
map<string, string> authors = { {"Joyce", "James"},
{"Austen", "Jane"},
{"Dickens", "Charles"} };
// 初始化muitimap或multiset
// 定义一个有20个元素的vector,保存0到
vector<int> ivec;
for (vector<int>::size_type i = 0; i != 10; ++i) {
ivec.push_back(i);
ivec.push_back(i); // 每个数重复保存一次
}
// iset包含来自ivec的不重复的元素;miset包含所有20个元素
set<int> iset(ivec.cbegin(), ivec.cend()); // ivec初始化iset,iset也只有10个元素,即ivec中的不同元素
multiset<int> miset(ivec.cbegin(), ivec.cend()); // ivec初始化miset,miset有20个元素
cout << ivec.size() << endl;
cout << iset.size() << endl;
cout << miset.size() << endl;
Ⅱ)关键字类型的要求
传递给排序算法的可调用对象必须满足与关联容器中关键字一样的类型要求。
Ⅲ)pair类型
一个pair保存两个数据成员。
pair<string, string> anon; // 保存两个string
pair<string, size_t> word_count; // 保存一个string和一个size_t
pair<string, vector<int>> line; // 保存string和vector<int>
pair<string, string> author{"James", "Joyce"}; // 保存两个string
for (const auto &w : word_count)
cout << w.first << "occurs" << w.second << ((w.second > 1) ? "times" : "time") << endl;
// 创建pair对象的函数
pair<string, int>
process(vector<string> &v) {
if (!v.empty())
return {v.back(), v.back().size()}; // 列表初始化
else
return pair<string, int>(); // 隐式构造返回值
}
// 用make_pair生成pair对象
if (!v.empty())
return make_pair(v.back(), v.back().size());
11.3 关联容器操作
key_type | 此容器类型的关键字类型 |
mapped_type | 每个关键字关联的类型,只适用于map |
value_type | 对于set,与key_type相同 对于map,为pair<const key_type, mapped_type> |
由于我们不能改变一个元素的关键字,因此这些pair的关键字部分是const的。
set<string>::value_type v1; // v1是一个string
set<string>::key_type v2; // v2是一个string
map<string, int>::value_type v3; // v3是一个pair<const string, int>
map<string, int>::key_type v4; // v4是一个string
map<string, int>::mapped_type v5; // v5是一个int
Ⅰ)关联容器迭代器
auto map_it = word_count.begin(); // 获得指向word_count中一个元素的迭代器,*map_it是指向一个pair<const string, size_t>对象的引用
cout << map_it -> first; // 打印此元素的关键字
cout << " " << map_it -> second // 打印此元素的值
map_it -> first = "new key"; // 错误!关键字是const的
++map_it -> second; // 正确:可以通过迭代器改变元素
注意:一个map的value_type是一个pair,我们可以改变pair的值,但不能改变关键字成员的值。
set的迭代器是const的:虽然 set类型同时定义了iterator和const_iterator类型,但两种类型都只允许只读访问set中的元素。
set<int> iset = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
set<int>::iterator set_it = iset.begin();
if (set_it != iset.end()) {
*set_it = 42; // 错误:set中的关键字是只读的
cout << *set_it << endl; // 正确!可以读关键字
}
遍历关联容器:
auto map_it = word_count.cbegin(); // 获得一个指向首元素的迭代器
while (map_it != word_count.cend()) {
cout << map_it -> first << " occurs " << map_it -> second << " times " <<endl; // 解引用
++map_it; // 递增迭代器,移动到下一个元素
}
关联容器和算法:通常不对关联容器使用泛型算法。在实际编程中,如果真的要对一个关联容器使用算法,要么将它当作一个源序列,要么当成一个目的位置。
Ⅱ)添加元素
关联容器的insert成员向容器添加一个元素或一个元素范围。由于map和set包含不重复的关键字,因此插入一个已经存在的元素对容器没有任何影响。
vector<int> ivec = {2, 4, 6, 8, 2, 4, 6, 8}; // ivec有8个元素
set<set> set2; // 空集合
set2.insert(ivec.cbegin(), ivec.cendZ()); // set2有4个元素
set2.insert({1, 3, 5, 7, 1, 3, 5, 7}); // set2有8个元素
word_count.insert({word, 1});
word_count.insert(make_pair(word, 1));
word_count.insert(pair<string, size_t>(word, 1));
word_count.insert(map<string, size_t>::value_type(word, 1));
Ⅲ)删除元素
// 删除一个关键字,返回市删除的元素数量
if (word_count.erase(removal_word))
cout << "ok: " << removal_word << " removed\n";
else cout << "oops: " << removal_word << " not found!\n";
Ⅳ)map的下标操作
map的下标运算符接受一个索引,获取与此关键字相关联的值。与其他下标运算符不同的是,如果关键字不在map中,会为它创建一个元素并插入到map中,关联值将进行值初始化。
map<string, size_t> word_count;
word_count["Anna"] = 1; // 插入一个关键字为Anna的元素,关键值进行值初始化;然后将1赋予它
// 使用下标操作的返回值
cout << word_count["Anna"]; // 用Anna作为下标提取元素,会打印出1
++word_count["Anna"]; // 提取元素,将其增1
cout << word_count["Anna"]; // 提取元素并打印,会打印出2
注意:与vector与string不同,map的下标运算符返回的类型与解引用map迭代器得到的类型不同。
Ⅴ)访问元素
set<int> iset = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
iset.find(1); // 返回一个迭代器,指向key == 1的元素
iset.find(11); // 返回一个迭代器,其值等于iset.end()
iset.count(1); // 返回1
iset.count(11); // 返回0
11.4 无序容器
如果关键字类型固有就是无序的,或者性能测试发现问题可以用哈希技术解决,就可以使用无序容器。
// 单词计数程序
unordered_map<string, size_t> word_count; // string到size_t的空map,关键字是string类型,值是size_t类型。
string word;
while (cin >> word)
++word_count[word]; // 提取word的计数器并将其加一
for (const auto &w : word_count)
cout << w.first << "occurs" << w.second << ((w.second > 1) ? "times" : "time") << endl;
参考教程:C++基本关联式容器
第十二章 动态内存
前言
除了自动和static对象外,C++还支持动态分配对象。动态分配对象的生存期与它们在哪里创建的是无关的,只有显式地被释放时这些对象才会被销毁。动态对象的正确销毁被证明是编程中极其容易出错的地方,为了更安全地使用动态对象,标准库定义了两个智能指针类型来管理动态分配的对象。当一个对象应该被释放时,指向它们的智能指针可以确保自动地释放他们。
静态内存用来保存局部的static对象、类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数内的非static对象。分配在静态或栈内存中的对象由编译器自动创建和销毁。对于栈对象,仅在其定义的程序块运行时才存在;static对象在使用之前分配,在程序结束时销毁。
除了静态内存和栈内存,每个程序还有一个内存池,这部分内存被称为自由空间或堆。程序用堆来存储动态分配的对象,即那些在程序运行时分配的对象。动态对象的生存期由程序来控制,当它们不再使用时,必须显式地销毁。
12.1 动态内存与智能指针
new:在动态内存中为对象分配空间并返回一个指向该对象的指针。
delete:接受一个动态对象的指针,销毁该对象,并释放与之关联的内存。
动态内存使用中常见的问题:内存泄漏(忘记释放内存)和产生引用非法内存的指针(释放尚有指针引用的内存)。
智能指针
shared_ptr:允许多个指针指向同一个对象
unique_ptr:独占所指向的对象
Ⅰ)shared_ptr类
// 智能指针也是模板,必须提供额外的信息:指针可以指向的类型
shared_ptr<string> pi;
shared_ptr<list<int>> p2;
if (p1 && p2->empty()) // 如果p1不为空,检查它是否指向一个空string
*p1 = "hi"; // 如果p1指向一个空string,解引用p1,将一个新值赋予string
make_shared函数:在动态内存中分配一个对象并初始化,返回指向此对象的shared_ptr。
shared_ptr<int> p3 = make_shared<int>(42); // p3指向一个值为42的int的shared_ptr
shared_ptr<string> p4 = make_shared<string>(10, '9'); // p4指向一个值为"9999999999"的string
shared_ptr<int> p5 = make_shared<int>(); // p5指向一个值值初始化的int,值为0
auto p6 = make_shared<vector<string>>(); // p6指向一个动态分配的空vector<string>
shared_ptr的拷贝和赋值
auto p = make_shared<int>(42); // p指向的对象只有p一个引用者
auto q(p); // p和q指向相同的对象,此对象有两个引用者
一旦一个 shared_ptr的计数器变为0,它就会自动释放自己所管理的对象。
auto r = make_shared<int>(42); // r指向的int只有一个引用者
r = q; // 递增q指向的对象的引用计数,递减r原来指向的对象的引用计数,r原来指向的对象已经没有引用者,会自动释放
shared_ptr自动销毁所管理的对象,还会自动释放所关联的内存。
// factory返回一个shared_ptr,指向一个动态分配的对象
shared_ptr<Foo> factory(T arg) {
return make_shared<Foo>(arg); // shared_ptr负责释放内存
}
void use_factory(T arg) {
shared_ptr<Foo> p = factory(arg); // 使用p,
} // 一旦离开作用域,p指向的内存会被自动释放掉
void use_factory(T arg) {
shared_ptr<Foo> p = factory(arg); // 使用p,
return p; // 当我们返回p时,引用计数进行了递增操作
} // 离开作用域,p指向的内存不会被自动释放掉
注意:1) 对于一块内存, shared_ptr类保证只要有任何shared_ptr对象引用它,它就不会被释放。
2) 如果将shared_ptr存放于一个容器中,而后不再需要全部元素,而只使用其中一部分,记得用erase删除不再需要的那些元素。
vector<string> v1; // 空vector
{
vector<string> v2 = {"a", "an", "the"};
v1 = v2; // 从v2中拷贝元素到v1
} // v2被销毁,其中的元素也被销毁;v1有三个元素,是原来v2中元素的拷贝
Blob<string> b1; // 空Blob
{
Blob<string> b2 = {"a", "an", "the"};
b1 = b2; // b1和b2共享相同元素
} // b2被销毁,但其中的元素不被销毁;b1指向最初由b2创建的元素
每个vector拥有其自己的元素,但我们拷贝一个vector时,原vector和副本vector中的元素是相互分离的。但某些类分配的资源具有与原对象 相独立的生存期。
class StrBlob {
public:
typedef std::vector<std::string>::size_type size_type;
StrBlob();
StrBlob(std::initializer_list<std::string> il);
size_type size() const { return data->size(); }
bool empty() const { return data->empty(); }
void push_back(const std::string &t) { data->push_back(t); }
void pop_back(); // 添加和删除元素
std::string& front();
std::string& back(); // 元素防访问
private:
std::shared_ptr<std::vector<std::string >> data;
// 如果data[i]不合法,抛出一个异常
void check(size_type i, const std::string &msg) const;
};
Ⅱ)直接管理内存
使用new动态分配和初始化对象
// 在自由空间分配的内存是无名的,无法为对象命名,而是返回一个指向该对象的指针
int *pi = new int; // pi指向一个动态分配的、未初始化的无名对象
string *ps = new string; // 初始化为空string
int *pi = new int(1024); // pi指向的对象的值为1024
string *ps = new string(10, '9'); // *ps为"9999999999"
vector<int> *pv = new vector<int>{0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
string *ps1 = new string; // 默认初始化为空string
string *ps = new string(); // 值初始化为空string
int *pi1 = new int; // 默认初始化;*pi1的值未定义
int *pi2 = new int(); // 值初始化为0;*pi2的值为0
auto p1 = new auto(obj); // p指向一个与obj类型相同的对象
auto p1 = new auto{a, b, c}; // 错误!括号中只能有单个初始化器
const int *pci = new const int(1024); // 分配并初始化一个const int
const string *pcs = new const string; // 分配并默认初始化一个const的空string
// 一个动态分配的const对象必须进行初始化
int *pi1 = new int; // 如果分配失败,new返回一个空指针,抛出std::bad_alloc;
int *pi1 = new (onthrow) int; // 如果分配失败,new返回一个空指针,不抛出异常
释放动态内存
delete p; // p必须指向一个动态分配的对象或是一个空指针
int i, *pi1 = &i, *pi2;
double *pd = new double(33), *pd2 = pd;
delete i; // 错误,i不是一个指针
delete pi1; // 未定义,pi1指向一个局部变量
delete pd; // 正确
delete pd2; // 未定义,pd2指向的内存已经被释放了
delete pi2; // 正确,释放一个空指针总是没有错的
const int *pci;
delete pci; // 正确,释放一个const对象
Foo* factory(T arg) {
return new Foo(arg); // 调用者负责释放此内存
}
void use_factory(T arg) {
Foo *p = factory(arg); // 使用p,但不delete它
} // p离开了它的作用域,但它指向的内存没有被释放
// 由内置指针(而不是智能指针)管理的动态内存在被显式释放前一直都会存在
// 修改:
void use_factory(T arg) {
Foo *p = factory(arg); // 使用p,但不delete它
delete p;
}
Foo* use_factory(T arg) {
Foo *p = factory(arg); // 使用p
return p; // 调用者必须释放内存
}
delete 之后重置指针值,这只是提供了有限的保护。
int *p =(new int(42)); // p指向动态内存
auto q = p; // p和q指向相同的内存
delete p; // p和q均变为无效
p = nullptr; // 指出p不再绑定到任何对象
Ⅲ)shared_ptr和new结合使用
shared_ptr<double> p1; // shared_ptr可以指向一个double
shared_ptr<int> p2(new int(42)); // p2指向一个值为42的int
shared_ptr<int> p3 = new int(42); // 错误!必须使用直接初始化方式,智能指针构造函数是explicit的。
Ⅳ)智能指针和异常
Ⅴ)unique_ptr
unique_ptr<double> p1; // unique_ptr可以指向一个double
unique_ptr<int> p2(new int(42)); // p2指向一个值为42的int
// ===========================================================================
unique_ptr<string> p1(new string("hi!"));
unique_ptr<string> p2(p1); // 错误!unique_ptr不支持拷贝
unique_ptr<string> p3;
p3 = p2; // 错误!unique_ptr不支持赋值
unique_ptr<string> p2(p1.release); // 将所有权从p1转移给p2
unique_ptr<string> p3(new string("hello!"));
p2.reset(p3.release()); // 将所有权从p3转移给p2, reset释放了p2原来指向的内存
// ===========================================================================
p2.release(); // 错误!p2不会释放内存,而且我们丢失了指针
auto p = p2.release(); // 正确,但是必须记得delete(p)
// ===================传递unique_ptr参数和返回unique_ptr=======================
unique_ptr<int> clone(int p) {
return unique_ptr<int>(new int(p)); // 返回unique_ptr
}
unique_ptr<int> clone(int p) {
unique_ptr<int> ret(new int(p)); // 返回一个局部变量的拷贝
return ret;
}
Ⅵ)weak_ptr
weak_ptr是一种不控制所指对象生存期的智能指针,它指向由一个shared_ptr管理的对象。将一个weak_ptr绑定到shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象。
// 创建一个weak_ptr时,要用一个shared_ptr来初始化
auto p = make_shared<int>(42);
weak_ptr<int> wp(p); // wp弱共享p;p的引用计数不变
// 由于对象可能不存在,不能使用weak_ptr直接访问对象,而必须调用lock
if (shared_ptr<int> np = wp.lock()) {
// 在if中,np与p共享对象
}
class StrBlobPtr {
public:
StrBlobPtr(): curr(0) { }
StrBlobPtr(StrBlob &a, size_t sz = 0): wptr(a.data), curr(sz) { }
std::string& deref() const;
StrBlobPtr& incr(); // 前缀递增
StrBlobPtr& decr(); // 前缀递减
private:
std::shared_ptr<std::vector<std::string>> check(std::size_t, const std::string&) const; // 若检查成功,check返回一个指向vector的shared_ptr
std::weak_ptr<std::vector<std::string>> wptr; // 保存一个weak_ptr,意味着底层vector可能被销毁
std::size_t curr; // 在数组中的当前位置
};
std::shared_ptr<std::vector<std::string>> StrBlobPtr::check(std::size_t i, const std::string &msg) const {
auto ret = wptr.lock(); // vector还存在吗
if (!ret)
throw std::runtime_error("unbound StrBlobPtr");
if (i >= ret->size())
throw std::out_of_range(msg);
return ret; // 否则,返回指向vector的share_ptr
}
std::string& StrBlobPtr::deref() const
{
auto p = check(curr, "dereference past end");
return (*p)[curr]; // (*p)是对象所指向的vector
}
StrBlobPtr& StrBlobPtr::incr()
{
check(curr, "increment past end of StrBlobPtr"); // 如果curr已经指向容器的尾后位置,就不能递增它
++curr; // 推进当前位置
return *this;
}
class StrBlobPtr;
class StrBlob {
friend class StrBlobPtr;
public:
StrBlobPtr begin() {
return StrBlobPtr(*this);
}
StrBlobPtr end() {
auto ret = StrBlobPtr(*this, data->size());
return ret;
}
};
12.2 动态数组
int *pia = new int[get_size()]; // 调用get_size确定分配多少个int
typedef int arrT[42]; // 表示42个int的数组类型
int *p = new arrT; // 分配一个42个int的数组;p指向第一个int
int *p = new int[42]; // 即使这段代码中没有方括号,编译器执行这个表达式的时候还是会用new[]
参考教程:C++ 动态内存