本文是阅读《C++ Primier Plus(第6版)》时的一些笔记,仅作记录
PDF文件网盘链接:https://pan.baidu.com/s/1SQLdaKCV5JlkPD61BlhDcQ?pwd=6999
1 初识C++
C语言的头文件使用扩展名.h。C++对老式C的头文件保留了扩展名.h,而C++头文件没有扩展名【有些C头文件被转换为C++头文件,重新命名去掉了扩展名,然后在文件名称前加上c(表明来自C语言)】
头文件通常包含的内容:函数原型、使用
#define或const定义的符号常量、结构声明、类声明、模板声明、内联函数,包含创建的自定义头文件时使用双引号
例如,C++版本的math.h为cmath【老式编译器不支持cmath】
#include <iostream> // 预处理命令使得iostream文件中的内容随源代码文件的内容一起被发送给编译器
using namespace std; //命名空间--防止函数冲突;简化程序
/*
假设Microflop和Piscine公司的产品都包含wanda()函数,这样使用wanda()函数是可以通过名称空间指定想使用哪个厂商的产品
Microflop::wanda()或者Piscine::wanda()
*/
int first_main() {
/*
* C++中,main()等价于main(void)【在C中,main()意味着对是否接受参数保持沉默】
* 注意: 虽然void main()和int main()在逻辑上是一致的,但是由于它不是当前标准强制的一个选项,因此在有些系统上不能工作
*
* main()函数是程序的入口,一个项目中有且仅有一个
* 例外:Windows编程中,编写一个动态链接库(DLL)模块,其他Windows程序可以使用。由于DLL模块不是独立的程,因此不需要main()
* 机器人中的控制器芯片——可能不需要main()【有隐藏的main(),调用_tmain()】
*/
int num = 66;
cout << "this is my first C++ file.\n";
cout << "the num is " << num << ", and please input a new num: "; // 打印之前,cout必须将整数形式的数字转换成字符串形式
cin >> num;
cout << "\nthe new num is " << num;
cout << endl;
cin.get(); // 等价于C程序中的getchar(); 防止控制台窗口一闪而过——按下[ENTER]才能退出
return 0;
/*
为什么使用变量之前必须声明?
有些语言(比如BASIC)在使用新名称是创建新的变量,而不用显示地声明【虽然短期地对用户友好,但是如果错误地拼写错了变量名,将在不知情的情况下创建一个新的变量】
*/
}
C++的内置整型:unsigned long、long、unsigned int、int、unsigned short、short、unsigned char、char、signed char、bool
C++11新增:unsigned long long和long long
表示各种整型的系统限制:climits文件
const限定符创建符号常量
int me;
const int* p1 = &me;//p1可变,*p1不可变,此时不能用*p1来修改,但是p1可以转向
int* const p2 = &me;//p2不可变,*p2可变,此时允许*p2来修改其值,但是p2不能转向。
const int* const p3 = &me;//p3不可变,*p3也不可变,此时既不能用*p3来修改其值,也不能转向
##指针和引用的区别很简单,就是引用更简洁,更安全。
因为引用声明时必须初始化。 引用更接近const指针,一旦与某个变量关联,就将一直效忠于他
## const指针可以接受const和非const地址,但是非const指针只能接受非const地址。
所以const指针的能力更强一些,所以尽量多用const指针,这是一种习惯。
//强制类型转换
(typeName) value 或者 typeName (value)
C++的内置浮点型:float、double、long double
表示各种整型的系统限制:cfloat文件
int wrens(43); // C++特有的初始化语法 C++建议在声明变量时对其初始化
int被设置为对目标计算机而言最为“自然”的长度【计算机处理起来最有效率】- 如果变量表示的值不可能为负(如文档中的字数),则可以使用无符号类型,可以表示更大的值
- 如果知道变量可能表示的整数值大于16为整数的最大可能值,则使用
long,即使系统上int为32位【否则程序一致到16位系统是就会无法正常工作】- 存储的值超过20亿,可以使用
long long- 如果
short比int小,可以使用short节省内存。通常仅当有大型整型数组时,才有必要使用short。【如果节省内存很重要,则应该使用short而不是int,即使它们长度一样(考虑到程序移植问题)】- 只需要用到一个字节
char
cout函数默认输出十进制的整数,而不管整数在程序中如何书写
int a = 42;
int b = 0x42; // 十六进制 hex
int c = 042; // 八进制 oct
cout << a << b << c; // 42 66 34
cout<<hex; cout<<oct; //不会在屏幕上输出任何内容,而是修改cout显示整数的方式
int debt = 7.2E12; // 由于int型变量无法存储 截断存储
C++的OOP概念——成员函数
类定义了如何表示和控制数据,成员函数归类所有,描述了操作类数据的方法
类ostream有一个put()成员函数用来输出字符,只能通过特定对象(这里的cout对象)调用
cout.put() // 成员函数调用
扩展字符集
8位char可以表示基本字符集,
wchar_t(宽字符类型)可以表示扩展字符集
wchar_t是一种整数类型,有足够的空间可以表示系统使用的最大扩展字符集【对底层类型的选择取决于实现,在一个系统中可能是unsigned short,在另外系统中可能是int】
C++11新增:
char16_t和char32_t【都是无符号的】
前缀u表示char16_t字符常量和字符串常量:u'C'、u"be good"与/u00F6形式的通用字符名匹配
前缀U表示char32_t字符常量和字符串常量:u'D'、u"be alright"/U0000222B形式
C++11:UTF-8根据编码的数字值,字符可能存储1~4八位组
C++11新增原始(raw)字符串,不会转义字符【原始字符R"(这里是"原始"字符串\n)"】
若原始字符串包含括号? 使用R"+*(标识原始字符串开头, 须使用)+*"标识结尾
wchar_t title[] = L"w_char string";
char16_t name[] = u"char16_t string";
char32_t name2[] = U"char32_t string";
C++的“新”类型:bool 【true/false】
/* 任何数字值或者指针都可以被隐式转换为bool【非0值转换为true、0转换为false】*/
bool start = -11; //true
bool end = 0; //false
const限定符【处理符号常量】 ¥注意:在声明时就初始化¥
const int Months = 12; // C语言中可以使用预处理方式 #define Months 12
// C++11 以{}方式初始化进行的转换
const int code = 66;
int x = 66;
char c1{31325}; // 超过表示范围,不允许
char c2 = {66}; //允许
char c3 {code}; // code是常量且在范围内,允许
char c4 = {x}; // 不允许,x是变量名,不是常量
x = 31325;
char c5 = x; // 这种初始化是允许的
初始化时使用auto,而不指定变量的类型,编译器把变量的类型设置成与初始值相同
auto n = 100; // n is int
auto x = 1.5; // n is double
auto y = 1.3e12L; // y is long double
2 数组与复合类型
-
数组(C++中被称为复合类型)的声明:
typeName arrayName[arraySize]arraySize须为整型常数或const值,也可以是常量表达式(如8*sizeof(int)) -
C风格字符串与string类字符串
使用iostream的cin函数输入字符串,每次只能输入一个单词【空格处断开】
面向行的类成员函数getline()和get()读取字符串:cin.getline()和cin.get()【这两个函数都读取一行输入,直到遇到换行符(getline丢弃换行符,get保留换行符在输入序列中)】- 输入的字符串比分配的空间长,会把余下的字符留在输入队列中,
getline()还会设置失效位,并关闭后面的输入 - 输入空行【当
get()读取空行后将设置失效位,接下来的输入被阻断,使用cin.clear()恢复输入】
使用
string类【位于名称空间std中,该类隐藏了字符串的数组性质】 -
创建和使用结构、共用体、枚举、指针
-
使用
new和delete管理动态内存 -
动态数组与动态结构
-
自动存储、静态存储和动态存储
-
vector和array类
#include <iostream>
#include <string>
#include <vector>
using namespace std;
int string_class();
int second_main() {
const int ArraySize = 20;
char name[ArraySize];
char food[ArraySize];
cout << "Enter your name:" << endl;
cin.getline(name, ArraySize);
cout << "Do you want some food?\n";
cin.getline(food, ArraySize);
cout << "Hi! " << name << ", I prepared " << food << " for you\n";
cout << "===================\n";
string_class();
return 0;
/*由于get()会保留换行符,所以两次调用get()会出错
cin.get(name, ArraySize);
cin.get(food, ArraySize); // 第一次调用后,换行符将留在输入队列中,因此第二次调用时看到的第一个字符就是换行符,get()便认为已到达行尾
需要将上述代码改变为下面的方式:
cin.get(name, ArraySize); // line 1
cin.get(); // 这两行等价于 cin.get(name, ArraySize).get();
cin.get(food, ArraySize); // line 2
*/
}
int string_class() {
char charr1[20]; // 创建空数组
char charr2[20] = "running"; // 创建初始化数组
string str1; // 创建空字符串对象
string str2 = "lifting"; // 初始化字符串对象
/*
*下面初始化方式合法
char charr2[20] = {"running"};
string str2 {"lifting"};
charr1 = charr2; // 不合法
str1 = str2; // 合法
string str3;
str3 = str1 + str2; //str3 是str1和str2的拼接
str1 += str2; // 将str2加到str1的尾部
C++使用string类处理字符串比C使用strcpy()、strcat()函数要简单
str1.size() or strlen(str1) 确定字符串的长度
将一行输入读到string对象中:
getline(cin, str1);
*/
cout << "Enter charr1:";
cin >> charr1;
cout << "Enter str1:";
cin >> str1;
cout << "here are some outputs:\n";
cout << charr1 << " - " << charr2 << " - " << str1 << " - " << str2 << endl;
cout << "the third letter in " << charr2 << " is " << charr2[2] << endl;
cout << "the third letter in " << str2 << " is " << str2[2] << endl;
return 0;
}
结构体中的位字段
注意区分结构体(struct,能同时存储多种数据类型)和共用体(union,语法和struct相似,但是只能同时存储一种数据类型)
union须有足够的空间存储最大的成员(union的长度位最大成员的长度),
用途是当数据项使用两种或以上的多种格式(不同时用),可节省空间【商品id可能int 可能string】
struct tor{ unsigned int SN : 4; bool g : 1; } // 这种方式指定了结构中的变量使用的位数
枚举enum提供创建符号常量的方式,可以代替const,语法与struct相似
enum spectrum {red, orange, yellow, green, blue, violet, indigo, ultraviolet}; // red登作为符号常量(枚举量) 值对应0~7
spectrum band; // 声明这种枚举类型的变量
band = blue; // 不强转的情况下,只能将定义枚举时的枚举量赋给这种枚举类型变量 band = 2000;不合法
band = red + 1; // 不合法,枚举只定义了赋值运算,没有定义算术运算
int color = red; // 枚举量是整型,可以被提升为int类型,但是int不能自动转换位枚举类型 band = 3;不合法
band = spectrum(3); //int值有效,可以通过强转赋值给枚举变量
band = spectrum(10); // 不会报错,但是未定义,结果不确定
-- 初始化枚举量时,可以自定义
enum bit{one=1, two = 200, three}; // three为201 指定的值必须为整数
-- 枚举也有取值范围,像上面定义的three为201,下面的代码是合法的 上限为大于最大值的、最小的2的幂再减去1【2^8=256 > 201,上限为255】
bit myflag;
myflag = bit(199); //199不是枚举值,但在201的范围里
C中使用库函数malloc()分配内存,C++可以这样做,但是有更好的方法:new运算符
int * pn = new int; // 运行阶段为一个int值分配一个未命名的内存、使用指针pn访问这个值【自动分配】
...
delete pn;//new分配,使用delete释放【配对使用,否则会发生内存泄露,被分配的内存无法再使用】空指针的delete是安全的
------和C作比较
int num;
int * pn = # //按照程序的写法编译,先初始化num的int类型变量,再把它地址赋给pn
------使用new创建动态数组
int * psome = new int[10]; // new返回第一个元素的地址(数组的首地址)
...
delete [] psome; //new创建的数组使用这种形式delete释放
// 若使用new []为一个实体分配内存,则应使用delete(没方括号)来释放
数组与指针的一些概念
// 多数情况下,C++将数组名解释成数组第一个元素的地址
short tell[10]; // 一个20字节长的数组
cout << tell <<endl; // 显示 &tell[0] 第一个元素的地址 (* short)
cout << &tell <<endl; // 整个数组的地址 (short(*) [20])
// 从数字上来说,两个地址相同;但概念上,&tell[0]是一个2字节内存块的地址,&tell是一个20字节内存块的地址
tell+1——地址+2;&tell+1——地址+20
------new创建动态结构 line 102
tor * ps = new tor;
ps->SN;
在C++中,存储类别是指用于描述变量的生命周期、作用域和存储方式的属性。C++中的存储类别分为四种:自动存储、静态存储、动态存储和线程存储[C++11]。其中,自动存储、静态存储和动态存储是三种最常用的存储类别。
-
自动存储
自动存储是C++中最常见的存储类别。在函数内部定义的变量默认是自动存储,这意味着它们的生命周期与函数调用的生命周期相同。当函数调用结束时,这些变量就会被销毁,它们的内存也会被释放。
自动存储变量的作用域仅限于函数内部,因此它们不能被其他函数或代码块访问。在C++中,可以使用auto关键字显式声明自动存储变量,但在实际编程中,auto通常可以省略。
自动变量通常存放在堆栈中,LIFO -
静态存储
静态存储是指变量的生命周期为整个程序运行期间,即使在函数调用结束后,它们仍然存在。
在C++中,可以在 函数外面定义 或者 用static关键字将变量声明为静态存储变量。静态存储变量在内存中分配固定的空间,它们的初始值为0或者初始化时指定的值,不同于自动存储变量,它们只会被初始化一次。
静态存储变量的作用域可以是函数内部或函数外部,它们可以被整个程序访问。在函数内部声明的静态变量可以在函数调用之间保留其值,因为它们的生命周期仍然是整个程序运行期间。 -
动态存储
动态存储是指在程序运行时手动分配和释放内存。在C++中,可以使用new和delete运算符来分配和释放动态存储空间【管理一个内存池,在C++中也被称为自由存储空间(free store)或堆(heap)】。
动态分配的内存可以用于创建动态数组和动态对象。与静态存储变量和自动存储变量不同,动态存储变量的生命周期由程序员显式控制,因此它们可以在需要时分配和释放内存空间,具有更大的灵活性。 -
ps. 线程存储
C++中还有一个特殊的动态存储类别——堆栈存储(或称为线程存储),它用于在多线程程序中保证变量在各个线程之间的独立性,本质上是一种静态存储变量和动态存储变量的混合体。
堆栈存储使用thread_local关键字来声明线程局部变量。
void func() {
thread_local int x = 0; // x是堆栈存储变量
x++;
cout << "Thread " << this_thread::get_id() << ": x = " << x << endl;
}
int main() {
thread t1(func);
thread t2(func);
t1.join();
t2.join();
return 0;
}
// 上面代码中两个线程的x值相互独立
模板类vector和array是数组的替代品
- vector
C++中的vector是一种动态数组,它可以在运行时动态增长和缩小大小,并且支持随机访问、快速插入和删除元素等操作。vector是C++标准库中的一个容器,定义在头文件中。
vector内部实现了一个数组,在创建vector对象时,可以指定容器中元素的类型和初始大小。当vector需要增加元素时,它会自动调整数组的大小,以容纳新元素。
vector还可以使用push_back()函数在尾部插入新元素,使用pop_back()函数删除尾部元素。 由于vector使用动态数组实现,因此在访问和修改元素时,vector的性能很高
vector<int> v; // 创建一个空的vector对象
vector<int> v(10); // 创建一个包含10个int类型元素的vector对象
vector<int> v(10, 0); // 创建一个包含10个int类型元素且值为0的vector对象
// 访问vector中的元素
vector<int> v = { 1, 2, 3, 4, 5 };
cout << v[0] << endl; // 输出第一个元素
cout << v.at(1) << endl; // 输出第二个元素
cout << v.front() << endl; // 输出第一个元素
cout << v.back() << endl; // 输出最后一个元素
// 向vector中添加或删除元素
v.push_back(6); // 在尾部添加元素
v.pop_back(); // 删除尾部元素
v.insert(v.begin() + 2, 10); // 在第三个位置插入元素10
v.erase(v.begin() + 1); // 删除第二个元素
// 获取vector中的信息
cout << v.size() << endl; // 输出vector中元素的个数
cout << v.capacity() << endl; // 输出vector中能够容纳的元素个数
cout << v.empty() << endl; // 判断vector是否为空
// 遍历vector中的元素
for (int i = 0; i < v.size(); i++) {
cout << v[i] << " ";
}
cout << endl;
for (auto x : v) {
cout << x << " ";
}
cout << endl;
// 改变vector的大小
vector<int> v = { 1, 2, 3 };
v.resize(5); // 将v的大小调整为5,新增的元素值默认为0
v.resize(2); // 将v的大小调整为2,删除末尾的元素
v.clear(); // 清空v中的元素
//可以使用reserve()函数来指定vector的容量。如果指定的容量小于vector当前的大小,则reserve()函数无效。
//复制vector,可以使用 = 运算符或assign()函数将一个vector复制给另一个vector。
vector<int> v1 = { 1, 2, 3 };
vector<int> v2 = v1; // 使用=运算符将v1复制给v2
vector<int> v3;
v3.assign(v1.begin(), v1.end()); // 使用assign()函数将v1复制给v3
vector<int> v1 = { 1, 2, 3 };
vector<int> v2 = { 4, 5, 6 };
v1.swap(v2); // 交换v1和v2中的元素
- array
array类是C++11标准中新增的一个容器,它可以用来存储一组固定大小的元素,这些元素的类型必须相同。与C++中的内置数组相比,array类提供了更多的便利和安全性。
创建array对象需要包含头文件<array>
array<int, 5> a1 = {1, 2, 3, 4, 5}; // 创建一个包含5个元素的int类型数组,并初始化为1、2、3、4、5
array<string, 3> a2 ={"hello", "world", "!"}; // 创建一个包含3个元素的string类型数组,并初始化为"hello"、"world"、"!"
C++11基于范围的for循环 对数组(或容器类,如vector、array)的每个元素执行相同的操作
double prices[3] = {4.99, 8.27, 2.33};
for(double x : prices) cout << x << endl; // 遍历数组
for(double &x : prices) x *= 0.8; // 修改数组元素
- 使用原始的cin进行输入【须有办法知道何时停止读取——哨兵字符】
char ch; int count = 0;
cin >> ch; // cin.get(ch); 注意!!!:C中修改ch的需要传入地址,即&ch,但在C++中,这样是有效的,函数将参数声明为引用
while(ch != '#'){ //统计输入字符的个数,并在屏幕上回显
cout << ch;
++count;
cin >> ch;
} ... //这样的方法不会将空格放入缓冲区, 若输入为:"hello world#",输出为"helloworld" 字符个数为10
-
使用
cin.get(char)【读入输入的每个字符,包括空格、制表符和换行符】 输出为"hello world" 字符个数为11 -
文件尾(EOF)条件(Ctrl+Z)【检测到EOF,cin将eofbit和failbit都设置为1,通过
cin.eof()查看eofbit,cin.fail()测试是否是EOF】
字符函数库 cctype
isalpha(ch) //判断ch是否为字符 0 or 1
if((ch>='a' && ch <='z') || (ch>='A' && ch <='Z')) 等价于 if(isalpha(ch))
isdigits() 是否为数字字符
isspace() 是否为空白(换行、空格、制表符)
ispunct() 是否为标点符号
文件I/O
- 写入文件
- 包含头文件fstream;
- 创建一个ofstream;
- 将该ofstream对象同一个文件关联起来;
- 同使用cout一样使用ofstream对象;
- 最后关闭文件
oftream outFile; ofstream fout;
outFile.open("file.txt"); // outFile用于写入file.txt 程序运行之前 file.txt 不存在
####
char filename[50];
cin >> filename;
fout.open(filename); // fout用于写入特定的文件(手动输入文件名)
- 读取文件
- 包含头文件fstream;
- 创建一个ifstream;
- 将该ifstream对象同一个文件关联起来;
- 同使用cin一样使用ifstream对象;
- 最后关闭文件
iftream inFile; ifstream fin;
inFile.open("file.txt"); // inFile用于读取file.txt
####
char filename[50];
cin >> filename;
fin.open(filename); // fin用于读取特定的文件(手动输入文件名)
// 读取文件 需要 inFile.eof() 判断是否读到文件尾
// 文件受损、硬盘故障 bad()会返回true。简单一些:使用good()方法先进行判断
3 函数
- 为什么需要函数原型?
C++需要函数原型是因为在编译器编译代码时,它需要知道每个函数的名称、返回类型和参数列表。
函数原型提供了这些信息,它定义了函数的名称、返回类型和参数列表,编译器可以根据这些信息来检查函数调用的正确性和合法性,确保函数的调用和定义一致。
int data[3][4] = {{1,2,3,4}, {2,3,4,5}, {3,4,5,6}};
int total = sum(data, 3);
// 函数原型为 int sum(int (*ar2)[4], int size); 或者 int sum(int ar2[][4], int size); 【(*ar2)[4]声明一个由4个指向int的指针组成的数组】
-
使用
const保护数组
向函数传参过程中传入数组名【数组第一个元素的地址】,即指针,很容易无意中修改数组的内容,可以在声明形参时使用关键字const -
将
const关键字用于指针
让指针指向const修饰的常量,这样可以防止使用该指针修改所指向的值
用const修饰指针,这样可以防止改变指针指向的位置
尽可能地使用
const【可以避免无意中修改数据的编程错误;能处理const和非const实参;const引用能够正确生成并使用临时变量】
尽可能地将引用形参声明为const
字符串的形参声明应为 char *
结构的形参可以为整个结构【假设定义结构struct student{...};,传入student zhangsan={...};】或者结构的地址【更省时间空间,能改变结构中的值】
函数指针
- 获取函数的地址:使用函数名即可【不带括号】
- 声明函数指针: 假设原型为
double pam(int);则指针类型声明:double (*pf)(int)
而double *pf(int)返回一个指向double类型的指针
C++内联函数
在C++中,内联函数是一种特殊的函数形式,它的定义和调用方式与常规函数相同,但它的实现方式和常规函数不同。
内联函数在编译时会被直接嵌入到调用该函数的地方,而不是在运行时被调用【地址跳转】。这种直接嵌入的方式可以减少函数调用的开销,从而提高程序的执行效率。
在C++中,使用关键字inline来声明内联函数。一般来说,内联函数通常用于简单、频繁调用的函数,比如一些简单的getter和setter函数、数值计算函数等。
这些函数在程序中被频繁调用,因此使用内联函数可以避免函数调用的开销,提高程序的执行效率。
使用内联函数需要注意以下几点:
- 内联函数的代码不能过于复杂,否则可能会导致程序的体积增大,反而降低程序的执行效率。
- 内联函数的实现必须放在头文件中,因为编译器需要在编译时直接嵌入函数的实现,而头文件是被包含在多个源文件中的。
- 内联函数的声明和实现必须一致,否则会导致编译错误。
- 内联函数不能递归调用自己,因为递归调用需要在运行时进行栈的操作,无法直接嵌入到调用处。
C++的引用变量
引用变量是一种特殊的变量类型,它相当于原变量的一个别名。使用&符号定义的【&在这里不是地址运算符,而是将rodents的类型声明为int &,即指向int变量的引用】
引用变量有以下几个特点:
- 引用变量必须在定义时进行初始化,并且不能再改变其引用的对象。
- 引用变量可以用于函数参数传递,它可以把函数调用的参数和函数内部的变量绑定在一起,从而可以改变函数外部变量的值。
- 引用变量可以用于赋值操作,将一个变量赋值给另一个引用变量时,它们会指向同一个地址空间。
- 引用变量是一种高效的变量类型,因为它避免了将变量拷贝到另一个变量中的开销,同时也避免了使用指针带来的风险。
引用变量的使用场景包括但不限于以下几种:
- 作为函数参数,可以使函数更加简洁高效,同时可以避免由于拷贝带来的开销。
- 在函数内部定义引用变量,可以用于引用函数外部的变量,从而实现变量的共享和修改。
- 用于实现操作符重载,使得程序更加简洁易懂。
#include <iostream>
using namespace std;
void swap1(int& a, int& b) { //引用变量作参数
int temp;
temp = a;
a = b;
b = temp;
}
void swap2(int* a, int* b) { // 指针做参数,传递地址
int temp;
temp = *a;
*a = *b;
*b = temp;
}
void swap3(int a, int b) { // 值传递
int temp;
temp = a;
a = b;
b = temp;
}
void main() {
int rats;
int& rodents = rats; // rodents作为rats的别名
int x = 30;
int y = 35;
cout << "x=" << x << " y=" << y << endl;
cout << "after swap1:" << endl;
swap1(x, y);
cout << "x=" << x << " y=" << y << endl;
cout << "after swap2:" << endl;
swap2(&x, &y);
cout << "x=" << x << " y=" << y << endl;
cout << "after swap3:" << endl;
swap3(x, y);
cout << "x=" << x << " y=" << y << endl;
}
C++新增:右值引用(rvalue reference)。这种引用可指向右值,使用&&声明
double&& rref = sqrt(36.00); // 不允许 double & cout<<rref; 为6.0
double j = 15.0;
double&& jref = 2.0 * j + 18.5; // 不允许 double & cout<<jref; 为48.5
返回引用
将函数的结果作为左值返回,返回引用的方式可以理解为函数返回一个指向某个变量的指针,这个指针可以用于修改变量的值。
与指针不同的是,返回引用的方式具有更高的安全性和简洁性,因为它不需要进行指针的解引用操作,而且在使用时不需要显式地进行地址取址和解引用操作。
需要遵守以下几个规则:
- 不要返回指向局部变量的引用。因为函数结束后,局部变量的内存空间会被释放掉,如果返回指向该变量的引用,则该引用将会指向一个无效的内存地址,这会导致未定义行为和不可预测的结果。
- 返回引用时,要确保返回的变量是持久的,也就是说,它在函数结束后仍然存在于内存中。这可以通过返回静态变量、全局变量或通过new运算符动态分配的变量的引用来实现。
- 如果返回的引用对象是一个成员变量,那么这个成员变量必须是对象的非临时成员变量,而且返回的引用必须在对象的生命周期内使用。
int& getRef(int& x)
{
return x;
}
int main()
{
int a = 10;
int& b = getRef(a); // 将a的引用赋值给了一个新的变量b,
b = 20;
cout << a << endl; // 输出20
return 0;
}
函数模板 <泛型>
C++函数模板(Function Template)是一种用于创建通用函数的机制,它可以实现对多种不同类型的数据执行相同的操作,提高了代码的复用性和可维护性。
函数模板是一种函数的抽象描述,不是真正的函数,它定义了一个通用的函数,其中使用了一个或多个类型参数。
在调用函数模板时,需要在尖括号内指定实际的类型,这些实际类型将替换函数模板中的类型参数,然后编译器会自动生成对应的函数。
template <typename T> // 或者 class T
T getMax(T a, T b)
{
return a > b ? a : b;
}
int main()
{
int x = 10, y = 20;
double p = 3.14, q = 2.71;
cout << "Max of " << x << " and " << y << " is " << getMax(x, y) << endl;
cout << "Max of " << p << " and " << q << " is " << getMax(p, q) << endl;
return 0;
}
显式具体化
C++的显式具体化(explicit specialization)是一种可以针对特定类型或值进行重载的方式。在C++中,我们可以使用模板(template)来定义通用的函数或类,使得它们可以适用于不同的类型或值。
然而,在某些情况下,我们可能需要对某些特定的类型或值进行特化处理,这时候就可以使用显式具体化。
具体来说,显式具体化是通过特定的语法来对函数或类模板中的某些特定类型或值进行重载,以提供更加精细的控制。
例如,我们可以定义一个泛型的Max函数,可以求出任意两个值中的较大值:
template<typename T>
T Max(T x, T y)
{
return (x > y) ? x : y;
}
但是如果我们要求两个字符串中较大的一个,这个函数并不能正确处理,因为>运算符并不支持字符串类型。为了解决这个问题,我们可以使用显式具体化来重载这个函数:
template<>
const char* Max<const char*>(const char* x, const char* y)
{
return (strcmp(x, y) > 0) ? x : y;
}
//这里我们对Max函数进行了显式具体化,指定了模板参数为const char*类型时应该调用的函数版本,
//即比较两个字符串的大小并返回较大的一个。
//当我们调用Max("hello", "world")时,编译器就会自动选择这个特化版本的函数。
需要注意的是,显式具体化只能用于函数模板和类模板中的某些具体类型或值的重载,而不能用于模板的部分特化。
此外,显式具体化也不应该过度使用,应该优先考虑通用的模板实现方式,只在必要的情况下使用显式具体化来提供特定类型或值的特化处理。
C++11关键字 decltype
int x;
decltype(x) y; // make y the same type as x
or
decltype(x+y) xpy=x+y;
C++(而不是C中),const限定符对默认的存储类型稍有影响,const全局变量的链接性为内部
const int a = 10; //等价于 static const int a = 10;
//如果想某个常量的链接性为外部的,可以使用extern关键字覆盖默认的内部链接性
extern const int a = 10;
多个const
在下面的例子中,第一个const防止字符串被修改,第二个const确保数组中每个指针始终指向它最初的字符串
const char* const months[12] = {"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"};
外部变量的多文件使用
在多文件程序,可以在一个文件(且只能在一个文件)中定义一个外部变量。使用该变量的其他文件必须使用
extern关键字声明它
int errors = 5; // flie1.c
int errors = 6; // file2.c 违反了单定义规则。file2.c向创建一个外部变量,程序中包含errors的两个定义 错误
// ----------------------------------------
int errors = 5; // flie1.c
static int errors = 6; // file2.c 使用static声明标识符errors链接性为内部,并非是外部声明
存储说明符
auto、register、static、extern、thread_local【C++11新增】、mutable
- 除了
thread_local可以和static或extern结合使用之外,同一个声明中不能使用多个说明符 - 可以用
mutable指出,即使结构(或类)变量为const,其某个成员也可以被修改
struct data{
char name[20];
mutable int access;
...
};
const data veep = {"moonjay", 0, ...};
strcpy(veep.name, "mao"); // 不允许
veep.access++; // 允许
cv-限定符(cv-qualifier)
const、volatile【该关键字的作用是为了改善编译器的优化能力】
程序在几条语句中两次使用了某个变量的值,则编译器可能不是让程序查找这个值两次,而是将这个值缓存到寄存器中。这种优化假设变量的值在这两次使用之间不会变化
如果不把变量声明为volatile,编译器会进行这种优化;否则就是告诉编译器不要进行这种优化
使用new运算符初始化
new运算符分配的空间是在堆上(动态内存)而不是栈上(静态内存);使用完之后,必须使用delete运算符将其释放,否则会造成内存泄漏。
int *pi = new int(6);
struct where{double x, double y};
where * one = new where{2.5, 3.4}; // C++11
int * ar = new int[4]{2,3,4,5}; // C++11 等价于 new(4*sizeof(int))
//分配函数【new和new []分别调用以下函数】:
void * operator new(std::size_t); // 对应 void operator delete(void *);
void * operator new[](std::size_t); // 对应 void operator delete[](void *);
定位new运算符 【需要引入头文件 #include <new>】
C++中的定位new运算符是一种可以在指定的内存地址上创建对象的运算符。在使用定位new运算符时,我们需要手动指定对象的内存地址,并将其传递给定位new运算符,以便在该地址上创建对象。
与常规的new运算符不同,定位new运算符不会分配新的内存,而是在指定的内存地址上创建对象,因此需要确保该内存地址是有效的,并且不与其他对象或变量冲突。
语法如下:
void* operator new(std::size_t size, void* ptr) noexcept;
// size参数表示要创建对象的字节数,ptr参数表示对象的内存地址。
这个运算符会在指定的内存地址上创建对象,并返回该地址的指针。
在使用定位new运算符时,我们需要手动调用对象的构造函数来初始化该对象,因为定位new运算符只负责在指定的内存地址上分配内存,而不会自动调用构造函数。
class MyObject
{
public:
MyObject(int value)
: m_value(value)
{
std::cout << "Constructing MyObject with value " << m_value << std::endl;
}
void printValue()
{
std::cout << "MyObject value: " << m_value << std::endl;
}
private:
int m_value;
};
int main()
{
char buffer[sizeof(MyObject)]; // 分配足够的内存
MyObject* obj = new (buffer) MyObject(42); // 在指定地址上创建对象
obj->printValue(); // 输出对象的值
obj->~MyObject(); // 调用对象的析构函数
return 0;
}
名称空间namespace【声明区域、作用域与潜在作用域】
using声明【使一个名称可用】和using编译指令【使所有的名称都可用】
未命名的名称空间仅在当前文件可用,且不能使用using声明和using编译指令,可以作为内部链接性的静态变量替代品
C++的名称空间namespace可以是全局的,也可以位于另一个名称空间中,但不能位于代码块中【默认在名称空间中声明的名称链接性为外部】
任何名称空间中的名称都不会与其他名称空间中的名称发生冲突
namespace A{int a; void func();}
namespace B{int a; void func();}
using namespace A; // using编译指令
using A::a; // using声明 && A::a称为限定的名称 &&
// 此时使用 using B::a会存在二义性,出错
4 类和对象
- 类的定义和实现
OOP的重要特性:抽象、封装和数据隐藏、多态、继承、代码可重用性
类规范由两个部分组成:类声明【以数据成员的方式描述数据部分,以成员函数的方式描述公有接口】;类方法定义【描述如何实现类成员函数】
class Stock { // 关键字 private【只能通过公共成员访问的类成员】和public【类公共接口的类成员(抽象)】
private: // 可以省略,默认是private
string company;
long share;
double share_val;
double total_val;
void set_tot() { total_val = share * share_val; }
public:
Stock();
Stock(const string& co, long n, double pr);
~Stock();
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show();
};
// 类中成员函数的定义: void Stock::buy(long num, double price){...}
// 对类中构造函数、析构函数的定义同上
- 公有类与私有类访问
- 类的数据成员、类方法(类成员函数)
- 类对象的创建与使用
- 类的构造函数和析构函数
- 类构造函数【没有返回值、也没有类型声明】,专门用于构造新对象、将值赋给它们的数据成员
- 默认构造函数【未提供显式初始值时用来创建对象的构造函数】
Stock::Stock(){} - 构造函数的参数【形参】不能与类成员名称同名
- 析构函数【构造函数创建对象后,程序负责跟踪该对象,直到它过期为止,对象过期时,程序自动调用特殊的成员函数:析构函数】
原型:~Stock();【无返回值和返回类型】
C++比C语言多出一个作用域解析运算符(::),放在变量名前,表示使用变量的全局版本
注意:无法使用对象调用构造函数,因为构造函数构造出对象之前,对象是不存在的
Stock::Stock(const string & co, long n, double pr)
//使用构造函数:显式调用
Stock s = Stock("world", 250, 1.25);
//隐式调用
Stock s("world", 250, 1.25);
//new 初始化
Stock *pstock = new Stock("world", 250, 1.25);
- const成员函数
const Stock land = Stock("this is name");
land.show(); // 按照对类的定义,编译器会拒绝这一行,因为show()的代码无法确保调用对象不被修改
//应该按照一下方式声明与定义:【新的语法保证函数不会修改调用对象】
void show() const; //声明
void Stock::show() const // 定义开头
this指针
每个成员函数(包括构造和析构)都有一个this指针,指向调用对象
const Stock & Stock::topval(const Stock & s) const{
//若s对象中的成员变量total_val较大返回s,否则返回当前对象
if(s.total_val> total_val) return s;
else returm *this;
}
- 对象数组
Stock mystuff[4]; // mystuff是包含四个Stock对象的数组
// 初始化方式
Stock mystuff[4] = {Stock(有参构造) or Stock()【默认】 ...}
- 类的作用域
类中定义的成员变量、成员方法作用域为整个类【类中已知、类外不可知】
可在不同类中使用相同的类成员名、方法名- 作用域为类的常量
//在类中定义 const int Months = 12; // 行不通,声明只是描述了对象的形式,没有创建对象,因此在实例化之前,没有存储值的空间 //可行方式 enum{ Months=12 }; double costs[Months]; // 对象中不包含枚举,Months只是一个符号名称,仅在编译时作数值替换 static const int Months = 12; double costs[Months]; // Months常量与其它静态变量存储在一起,而不是存在对象中- 作用域内枚举
传统枚举存在两个枚举定义中的枚举量可能发生冲突enun egg {small, midium}; enum t_shirt {small, midium}; // 编译失败,egg和t_shirt位于相同作用域,发生冲突 // C++11提供新枚举,枚举量作用域为类 enun class egg {small, midium}; enum class t_shirt {small, midium}; // 也可以使用关键字struct代替class //使用 egg choice = egg::small; //C++11作用域枚举的默认底层类型为int,还可以通过 enun class : short egg {small, midium}; // 指定底层类型为short
- 抽象数据类型【类的概念适合ADT】
5 类的使用
对类成员使用动态内存分配【C++在程序运行时决定内存分配,而不是在编译时决定】
class StringBad {// 该类是一个糟糕的类,num_string可能为负值【程序通常会在显式有关还有-1个对象的信息之前中断,有些机器将报告通用保护错误(GPF)——程序试图访问禁止它访问的内存单元】
private:
char* str; // 使用char指针表示字符串,类声明本身没有为其分配存储空间【在构造函数中使用new为其分配,避免预先定义字符串长度】
int len;
static int num_string;
public:
StringBad(const char * s);
StringBad();
~StringBad();
friend ostream& operator<<(ostream& os, const StringBad& st);
};
int StringBad::num_string = 0; // 初始化静态类成员,注意不能在类声明中初始化【类声明描述如何分配内存但是不分配内存】
StringBad::StringBad(const char * s){ // 有参构造
len = strlen(s);
str = new char[len + 1]; // new分配存储空间
strcpy(str, s);
num_string++;
cout << num_string << ": \"" << str << "\" object created\n";
}
StringBad::StringBad() { // 默认构造函数
len = 4;
str = new char[len]; // new分配存储空间
strcpy(str, "C++");
num_string++;
cout << num_string << ": \"" << str << "\" default object created\n";
}
StringBad::StringBad() { // 析构函数
cout << "\"" << str << "\" object deleted\n";
--num_string;
cout << num_string << " left\n";
delete[] str; // 构造函数使用new分配字符串空间需要在析构函数中使用delete释放
}
ostream& operator<<(ostream& os, const StringBad& st) {
os << st.str;
return os;
}
-
运算符重载
operator+()重载+运算符、operator*()重载*,其中op必须是C++中有效的运算符A = B * 2.75;其中A、B为Time类对象 编译器会转换为A = B.operator*(2.75);
从概念上来说,B * 2.75等价于2.75 * B,但是后者不对应成员函数,2.75不是Time类
解决方式1:须按
B * 2.75这种方式编写,(sever-friendly,client-beware)。
解决方式2:非成员函数(参数顺序限制)】
【对于非成员重载运算符函数来说,运算符表达式左边对于运算符函数第一个参数、右边对应第二个参数】重载之后就可以实现对象的
+、*
重载限制:- 最好不要将
-重载为+,虽然可行; - 不能违反重载之前的运算规则【如把
%重载为一个操作数】; - 不能创建新运算符【如定义
operator**()表示求幂】 - sizeof、
.【成员运算符】、*指针运算符、::作用与解析运算符、?:条件运算符
- 最好不要将
-
友元函数
通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限-
创建友元
将函数原型放在类声明中,并加上关键字friend
【虽然在类中声明的,但它不是成员函数,因此不能使用成员运算符调用】
【虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同】
friend Time operator*(double m, const Time & t); -
编写函数定义(因为不是成员函数,所以不使用
Time::限定符,定义也不需要使用friend关键字)
Time operator*(double m, const Time & t){...}
对于上面的例子,使用类方法和友元函数,可以用同一个用户接口表达(B * 2.75等价于2.75 * B)
友元函数没有违背OOP隐藏数据的原则,实际上只有类声明可以决定哪个函数是友元,友元函数作为类的扩展接口的组成部分,和类方法是表达类接口的两种不同机制
【友元函数重载运算符需要两个参数,成员函数重载只需要一个参数,且两者不能冲突】
-
-
重载
<<运算符,便于输出【常用的友元函数】
假设trip是Time类的对象,能否不调用show()成员函数,直接cout << trip;呢?- 第一种方法:
void operator<<(ostream & os, const Time & t){ // 另一个ostream对象是cerr,将输出发送到标准输出流(默认为显示器) os << t.hour << " hours, " << t.minutes << " minutes"; }- 第二种方法:
方法一存在问题,cout << trip;可以正常工作,但是cout << "trip time:"<<trip;却无法工作
ostream & operator<<(ostream & os, const Time & t){ // 另一个ostream对象是cerr,将输出发送到标准输出流(默认为显示器) os << t.hour << " hours, " << t.minutes << " minutes"; return os; } -
类的自动转换和强制类型转换
【C++不会自动转换不兼容的类型。比如:int * p = 10;是非法的(左边指针类型,右边int类型)】
强转可行:int * p = (int *)10;
将类的构造函数用作自动类型转换【这种自动特性并非总是合乎需要,可能导致意外的类型转换】,C++的关键字explicit,用于关闭这种自动特性
关于explicit的详细解释:https://blog.youkuaiyun.com/weixin_57165154/article/details/124238651
- 类转换函数
强制类型转换int()、double()等函数 原型为operator typeName();- 转换函数必须是 类方法
- 转换函数不能指定返回类型
- 转换函数不能有参数
int fifth_main() {
double direction;
srand(time(0)); // 设置随机种子, srand()允许覆盖默认的种子值
direction = rand() % 360; // rand()得到的其实是伪随机数
cout << "current time: " << time(0) << " current direction: " << direction;
return 0;
}
-
隐式和显式复制构造函数
C++自动提供了成员函数:默认构造、析构函数,复制构造函数,赋值运算符【如对象之间的赋值】,地址运算符
复制构造函数用于将一个对象复制到新创建的对象中,它用于初始化过程中(包括按值传递参数),而不是常规的赋值过程中,原型:Class_name(const Class_name &)何时调用?有何功能?
新建一个对象并将其初始化为同类现有对象,复制构造函数都将被调用【假设motto是StringBad对象】——复制构造函数不包含num_string++行为,析构将复制对象释放也会造成num_string自减,从而造成num_string变成负值1 StringBad ditto(motto); 2 StringBad metto = motto; 3 StringBad also = StringBad(motto); 4 StringBad* p = new StringBad(motto);默认的复制构造函数逐个复制非静态成员(成员复制称为浅复制),复制的是成员的值,【静态成员num_string不受影响,因为它属于整个类,而不是各个对象】
声明2StringBad metto = motto;等价于StringBad metto; metto.str = motto.str; metto.len = motto.len;
注意上面的对象复制(按值复制),metto.str = motto.str实际是复制指向字符串的指针【析构函数调用时,释放了对象motto地址,再次打印metto时motto.str已经被释放】
赋值运算符(对象):StringBad & StringBad::operator=(const StringBad &) -
在构造函数中使用new所必须完成的工作
- 如果在构造函数中使用new来初始化指针成员,则在析构函数中应使用
delete - 若有多个构造函数,须以相同的方式使用
new(都带中括号或者都不带)【因为只有一个析构函数,考虑兼容】 - 可以在一个构造函数中使用new初始化指针,而在另一个构造函数中将指针初始化为空(0或C++11中的nullptr)
- 应定义一个复制构造函数,通过深度复制将一个对象初始化为另一个对象。类似如下代码
String::String(const String & st){ num_string++; len = st.len; str = new char[len+1]; // 深度copy strcpy(str, st.str); }- 应定义一个赋值运算符,通过深度复制将一个对象复制给另一个对象。类似如下代码
String & String::operator=(const String & st){ if(this==&st) return *this; // 对象赋值给它自己 delete[] str; // 释放旧string len = st.len; str = new char[len+1]; strcpy(str, st.str); return *this; } - 如果在构造函数中使用new来初始化指针成员,则在析构函数中应使用
-
使用静态类成员
使用关键字static声明。不能通过对象调用静态成员函数【静态成员函数甚至不能使用this指针】
如果静态成员函数是在公有部分声明的,可以使用类名和作用域解析运算符调用
比如:给String类添加原型/定义static int how(){return num_string;}
调用方式为int count = String::how();
静态成员方法只能访问类中的静态成员(num_string),不能访问类的私有变量(str、len) -
有关返回对象的说明
- 返回指向const对象的引用
使用const引用的常见原因是旨在提高效率,何时采用有限制
如果函数返回(通过调用对象的方法或将对象作为参数)传递给它的对象,可以通过返回引用来提高效率
比如设计程序返回两个Vector对象较大的一个:
【返回对象将调用复制构造函数,而返回引用不会,所以version 2效率更高】Vector Max(const Vector & v1, const Vector & v2){...} // version 1 const Vector & Max(const Vector & v1, const Vector & v2){...} // version 2 - 返回指向非const对象的引用
两种常见的返回非const对象情形:重载赋值运算符(旨在提高效率)以及与cout一起使用的<<运算符(须这样做) - 返回对象
如果被返回的对象是被调用函数中的局部变量,则不应按引用的方式返回它【在被调用函数执行完毕时,局部对象将调用其析构函数,因此当控制器回到调用函数时,引用指向的对象将不存在】
示例: Vector类重载+运算符Vector Vector::operator+(const Vector & b) const{ return Vector(x+b.x, y+b.y); } Vector force1(50, 60); Vector force1(10, 70); Vector net; net = force1+force2; - 返回const对象
上面的代码写成:force1+force2 = net;也是合法的。force1+force2会产生一个临时对象
对于net = force1+force2;计算两个对象之和,得到一个临时对象,该临时对象被赋给net对象
若担心误用和滥用,将返回类型声明为const Vector,这样force1+force2 = net;变成非法的语句
- 返回指向const对象的引用
-
使用指向对象的指针
String * favorite = new String(sayings[choice]); // String为自定义字符串类【此语句的new不是为字符串分配内存,而是为对象分配内存】
指针favorite指向new创建的未被命名对象。使用对象sayings[choice]来初始化新的String对象,会调用复制构造函数(参数为(const String &))
class Act{...};
...
Act nice; // 外部对象
...
int main(){
Act* pt = new Act; // 动态对象
{
Act up; // automatic object
...
} // 执行到定义代码块末尾,自动调用对应up对象的析构函数
delete pt; // new创建的使用delete,调用动态对象 *pt 的析构函数
...
} // 程序结束调用静态对象nice的析构函数
// 实现队列ADT
class Queue{
private:
// class scope 定义
struct Node{ // 在类中嵌套结构,可以让结构的作用域为整个类【非必须,可以在类外声明】
Item item; // 使用typedef定义,存储节点的数据
struct Node* next; }
enum { Q_SIZE = 10; }
// Queue类的私有变量
Node * front;
Node * rear;
int items; // 当前时队列中的第几个items
const int qsize; // 队列容量
...
public:
...
};
// 类方法定义
Queue::Queue(int qs) : qsize(qs){
// 因为qsize是常量,可以初始化却不能赋值,使用该条语句的语法(成员初始化列表),变量之间逗号隔开。【const类成员、被声明为引用的类成员须使用该语法】
// 引用与const数据类似,只能在创建时被初始化。对于简单数据成员:front、rear、items,可以按下面两行代码在函数体中初始化或者:Queue::Queue(int qs) : qsize(qs),front(NULL),rear(NULL),items(0){}——本就是类成员,这样初始化效率更高
// 注意:成员初始化列表的格式只能用于构造函数;
front = rear = NULL;
items = 0;
}
-
is-a关系的继承
基类指针可以在不进行显示类型转换的情况下指向派生类对象;基类引用可以在不进行显示类型转换的情况下引用派生类对象。【反过来不行】RatedPlayer rp(111, "mello", true); TableTennisPlayer & rt = rp; TableTennisPlayer * rp = &rp; rt.Name(); pt->Name();基类指针(引用)只能用于调用基类方法【不能使用rt或rp调用派生类的ResetRating()或Rating()方法】
继承可以在基类的基础上添加属性,但不能删除基类的属性 -
以公有方式从一个类(基类)派生出另一个类(派生类)
多态公有继承:在派生类中重新定义基类的方法;使用虚方法银行基本支票账户Brass类,代表更高级一些的支票账户BrassPlus类,添加了透支特性-
BrassPlus在Brass的基础上添加了一些私有数据成员和公有成员函数
-
Brass和BrassPlus都声明了ViewAcct()和WithDraw()方法,但是行为不同
基类和派生类的限定名不同:Brass::ViewAcct()和BrassPlus::ViewAcct() -
Brass在声明ViewAcct()和WithDraw()时使用了关键字virtual【虚方法】,派生类重定义基类方法时,最好是虚方法
一般在基类中将派生类会重新定义的方法声明为虚方法,这样派生类将自动成为虚方法
如果方法通过引用或指针而不是对象调用的,它将确定使用哪一种方法。Brass dom(参数); BrassPlus dot(参数); Brass & b1_ref = dom; Brass & b2_ref = dot; // 换成指针行为类似 // 若ViewAcct()未使用virtual b1_ref.ViewAcct(); // use Brass::ViewAcct() b2_ref.ViewAcct(); // use Brass::ViewAcct() // 若ViewAcct()使用了virtual b1_ref.ViewAcct(); // use Brass::ViewAcct() b2_ref.ViewAcct(); // use BrassPlus::ViewAcct()- 如果没有使用virtual,程序将根据引用类型或指针类型选择方法 - 如果使用virtual,程序将根据引用或指针指向的对象类型来选择方法 -
Brass还声明了一个虚析构函数,这样做是为了确保释放派生对象时,按正确的顺序调用析构函数
-
-
保护访问(
protected)
protected关键字表示成员变量或成员函数只能在该类及其子类中被访问。在类外部,无论是普通函数还是其他类,都不能访问protected成员。 -
虚成员函数
假设要同时管理Brass和BrassPlus账户,希望能用同一个数组来保存Brass和BrassPlus对象【不可能,因为数组中的元素类型须相同】
可以通过创建指向Brass的指针数组,这样数组中所以元素的类型都相同【因为时公有继承模型Brass指针既可以指向Brass对象、也可以指向BrassPlus对象】使用虚须析构函数可以确保正确的析构函数序列被调用【引用或者指针指向的对象,调用其虚构函数】,
如果不使用虚析构函数,使用Brass *或Brass &声明对象的指针或引用,即使指针指向BrassPlus对象,也只有Brass的析构函数被调用注意:
- 将构造函数声明为virtual没什么意义
- 析构函数应为virtual,除非类不用做基类【通常应给基类提供一个析构函数,即使他并不需要析构函数】
- 友元不能是虚函数,因为友元不是类成员【只有类成员才能是虚函数】
- 派生类若没有重新定义函数,将使用基类的版本
- 重新定义将隐藏方法
class A{
public:
virtual void show(int n) const;
// 或者 virtual A & show(int n) const;
...
};
class B : public A{
public:
virtual void show() const; // 如果想重载,确保与基类原型一致
// 或者 virtual B & show(int n) const; // 这样是有效的,也要保证函数参数一致
...
};
//上述代码可能出现编译器警告。
B obj_B; obj_b.show();//是有效
obj_b.show(23);//是无效的
//重新定义不会生成函数的两个重载版本,而是隐藏了基类接受一个int参数的版本
- 早期(静态)联编与晚期(动态)联编【前者效率更高】
将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编(binding)【C语言中每个函数名都对应一个不同的函数,在C++中,由于函数重载的原因,更为复杂】
C/C++编译器可以在编译过程完成这种联编(静态/早期联编),然而虚函数使得这项工作变得更困难,编译器须生成能够在程序运行时选择正确的虚方法的代码(动态联编)
虚函数的实现依赖于虚函数表(vtable)和虚函数指针(vpointer)。虚函数表是一个指针数组,存储着类的虚函数地址,每个类只有一个虚函数表,其大小和层次结构相关。
虚函数指针是一个指向虚函数表的指针,它在类对象中存储,通过它来访问虚函数表中的虚函数地址。
总之,使用虚函数时在内存和执行速度上都有一定的成本:
- 每个对象都将会增大,增大量为存储地址的空间
- 对于每个类,编译器都创建一个虚函数地址表(数组)
- 对于每个函数调用,都需要执行一项额外的操作:到表中查找地址
-
抽象基类(Abstract Base Class)
比如椭圆(Ellipse)和圆(Circle)有很多共同点,圆可以看作是特殊的椭圆(长轴等于短轴),假设从Ellipse派生Circle,会显得十分笨拙
而分别定义两个类也忽略了两者之间的共同点。
解决办法:从Ellipse和Circle抽象出它们的共性,将这些特性放大一个ABC中,然后从ABC派生Ellipse和Circle【将两者共性放在ABC中】
假设ABC包含中心点的坐标x,y和一些公共方法:Area()等 -
纯虚函数
Area()函数对两个类来说是不同的【求面积公式有差别】,甚至不能在ABC中实现Area()方法,因为它没有包含必要的数据成员(Ellipse的长短轴a,b;Circle的半径r)
C++通过纯虚函数提供未实现的函数:virtual double Area() const = 0; // 纯虚函数声明结尾处为"=0",可理解为ABC提供给派生类的原型函数
【当类中包含纯虚函数时,不能创建该类的对象】
【真正的ABC,须至少有一个纯虚函数】
#include <iostream>
#include <cstdlib> // rand()、srand() prototype
#include <ctime> // time() prototype
#include <cstring>
using namespace std;
// Webtown俱乐部跟踪乒乓球会会员,首先需要设计一个简单的基类 TableTennisPlayer
class TableTennisPlayer {
private:
string name;
bool hasTable;
public:
TableTennisPlayer(const string& n = "none", bool ht = false);
void Name() const; // 显示player名字
bool HasTable() const{ return hasTable; };
void ResetTable(bool v) { hasTable = v; };
};
TableTennisPlayer::TableTennisPlayer(const string& n, bool ht) : name(n), hasTable(ht) {} // 构造函数 成员初始化列表
void TableTennisPlayer::Name() const {
cout << name;
}
// Webtown俱乐部的一些成员参加过比赛,需要一个包含成员在比赛中的比分,从基类派生
class RatedPlayer : public TableTennisPlayer {
// 该语句表明TableTennisPlayer是一个公有基类【公共派生】,派生类对象包含基类对象
// 基类的公有成员成为派生类的公有成员,基类的私有部分也将成为派生类的一部分,但只能通过基类的公有(public)和保护(protected)方法访问
// 派生类需要自己的构造函数;可以根据需要添加额外的数据成员和成员函数
private:
unsigned int rating;
public:
RatedPlayer(unsigned int r = 0, const string& n = "none", bool ht = false);
RatedPlayer(unsigned int r, const TableTennisPlayer& tp); // 会调用基类的复制构造函数
unsigned int Rating() const { return rating; }
void ResetRating(unsigned int r) { rating = r; }
};
RatedPlayer::RatedPlayer(unsigned int r, const string& n, bool ht) : TableTennisPlayer(n, ht) { rating = r; } // 创建派生类对象程序首先创建基类对象
// 如果不调用基类构造函数:RatedPlayer::RatedPlayer(unsigned int r, const string& n, bool ht = false){ rating = r; }
// 上面的注释代码等价于 RatedPlayer::RatedPlayer(unsigned int r, const string& n, bool ht = false) : TableTennisPlayer(){ rating = r; }
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer& tp) : TableTennisPlayer(tp) { rating = r; }
// 派生类构造函数应通过成员初始化列表将基类信息传递给基类构造函数;派生类构造函数应初始化派生类新增的数据成员;
int fifth_main_2() {
TableTennisPlayer* p1 = new TableTennisPlayer("moonjay", false); // or使用 TableTennisPlayer p1("moonjay", true);
RatedPlayer* rp2 = new RatedPlayer(1140, "wrj", true);
p1->Name(); // p1.name();
if (p1->HasTable()) cout << ": has a table.\n";
else cout << ": hasn't a table.\n";
rp2->Name();
if (rp2->HasTable()) cout << ": has a table.\n";
else cout << ": hasn't a table.\n";
cout << "is rated player; and rating :" << rp2->Rating();
delete p1;
delete rp2;
return 0;
}
- 如果定义了某种构造函数,默认构造函数须自己提供。复制构造函数接受其所属类的对象作为参数,如:
Star(const Star &);
使用复制构造函数的几种情况:- 将新对象初始化为一个同类对象
- 按值将对象传递给函数
- 函数按值返回对象
- 编译器生成临时对象
如果编译器没有使用(显示或隐式)复制构造函数,编译器将提供其原型,但是不包括函数定义
- 赋值运算符
默认的赋值运算符用于处理同类对象之间的赋值 - 使用const
A::A(const char * s){...} // const确保方法不修改参数
void A::show() const{...} // const确保方法不修改它调用的对象 这里的const表示const A * this,this指向调用的对象
- 构造函数、析构函数、赋值运算符(成员
=,非op=)都是不能被继承的。友元函数不是类成员,因此也不能被继承
6 进一步了解类
-
C++中的代码重用。C++的一个主要目标是促进代码重用,公有继承使用实现机制之一
- 还可以使用这样的类成员:本身是另一个类的对象【该方法称为包含(containment)、组合(composition)或层次化(layering)】
- 另一种方法是使用私有或保护继承。has-a关系【公有继承时is-a关系】
包含、私有继承与保护继承用于实现has-a关系,即新的类将包含另一个类的对象
-
包含对象成员的类 (Student类)
注意:通常应使用包含来建立has-a关系;如果新类需要访问原有类的保护成员,或需要重新定义虚函数,则应该使用私有继承 -
模板类
valarray【需要包含头文件】
这个类用于处理数值(或具有类似特性的类)。模板类在声明对象时须指定具体的数据类型。
【如果不使用这个模板类的话,类中用到int、double等类型的数组需要定义相关类(成员与方法)】
valarray<double> q_values; // 声明一个double数组,size为0
valarray<int> v1(8); // an array of 8 int elements
valarray<int> v2(10, 8); // an array of 8 int elements, each set to 10
double gpa[3] = {3.1, 2.4, 6.4};
valarray<double> v3(gpa, 2); // an array of 2 int elements, initialized to the first 2 elements of gpa
valarray<int> v4 = {1, 2, 3, 5};
vector和array类也有类似语法,不过valarray支持的算术运算更多
valarray类的一些方法:operator[]()——访问元素;size()——包含元素数;sum()——所有元素总和;max/min()——最大最小值
-
私有和保护继承
-
私有继承,基类的公有成员和保护成员都将成为派生类的私有成员【基类方法将不会成为派生对象公有接口的一部分,但可以在派生类的成员函数中使用它们】
包含将对象作为一个命名的成员对象添加到类中,而私有继承将对象作为一个未被命名的继承对象添加到类中。【使用子对象表示通过继承或包含添加的对象】
私有继承提供的特性与包含相同:获得实现,但不获得接口
代码实现(以改写Student类为例):class Student : private std::string, private std::valarray<double>{ // 使用多个基类继承 public: ...... };对于继承类,使用类名而不是成员名来标识构造函数:
// ArrayDb时std::valarray<double>的别名 Student(const char* str, const double* pd, int n) : std::string(str), ArrayDb(pd, n){} // 对比Student类的最后一个构造函数(析构函数上)继承类使用作用域解析运算符可以访问基类的方法,但要使用基类对象本身,如何做?
比如 Student类的包含版本 实现了Name()方法——返回string对象成员name
但使用私有继承时,该string对象没有名称,那么Student类的代码如何访问内部的string对象?
使用强制类型转换(将Student对象转换为string对象),因为Student类时从string类派生而来的;结果为继承而来的string对象。
this指向用来调用方法的对象,*this就是这个对象(该例中为Student的对象),为避免调用构造函数创建新的对象,可使用强制类型转换来创建一个引用:
const string& Student::Name() const{ return (const string&) *this; } -
保护继承【私有继承的变体】
基类的公有成员和保护成员都将成为派生类的保护成员。
私有继承的第三代类将不能使用基类的接口(基类的public方法在派生类中变为private);保护继承的第三代可以使用基类的接口(由基类的public变为派生类的protected)
class Student : protected std::string, protected std::valarray<double>{ ... };
-
-
使用using重新定义访问权限
- 第一种方式:假设希望Student类能够使用valarray类的sum()方法,可以在Student声明sum方法,然后定义如下
double Student::sum() const{ return std::valarray<double>::sum(); } // 在Student的public方法中使用valarray的private方法- 第二种方式:将函数调用包装在另一个函数调用中,解释希望通过Student类使用valarray的min()和max(),加入声明如下:
class Student : private std::string, private std::valarray<double>{ // 注意:using声明只适用于继承而不适用与包含 ... public: using std::valarray<double>::operator[]; // 将使得两个版本(const和非const)都可用。便可删除Student::operator[]()的原型和定义 using std::valarray<double>::min; using std::valarray<double>::max; // 这样声明使得它们像是Student的公有方法一样 ... } //调用时 cout<<"high score: "<<ada[i].max()<<endl;' -
多重继承(MI):由多个直接基类的类,同单继承一样是is-a关系
注意:需用关键字public限定每一个基类,否则编译器默认为私有派生class SingingWaiter : public Waiter, Singer { ... }; // Singer is a private baseMI可能带来的问题:从两个不同基类继承来的同名方法;从两个或更多相关基类那里继承同一个类的多个实例
【假设Waiter和Singer都继承自Worker,基类Worker包含set和show方法——设置和显示fullname和ID,派生类Waiter和Singer重载这两个方法】main方法中: Waiter a(有参构造); Singer b(有参构造); Waiter c_temp; Singer d_temp; // 初始化对象,后两个默认构造函数 Worker* pw[4] = {&a, &b, &c_temp, &d_temp}; // 使用Waiter指针调用 Waiter::Show() Waiter::Set();Singer指针调用Singer::Show() ... 没问题但是添加一个从Waiter和Singer派生出的SingingWaiter类将带来问题:多少Worker?调用哪个类的方法?
由于Singer和Waiter都继承自Worker,因此SingingWaiter将包含两个Worker组件,在将派生类对象赋给基类指针是出现二义性:SingingWaiter ed; // Worker* pw = &ed; // 二义性 Worker* pw1 = (Waiter *)&ed; // 使用类型转换指定对象 Worker* pw2 = (Singer *)&ed;这样会使得使用基类指针来引用不同得对象(多态性)复杂化。问题在于为什么需要Worker对象得两个拷贝?
C++引入虚基类使MI成为可能
-
虚基类【使得从多个类(它们得基类相同)派生出的对象只继承一个基类对象】
在类声明是使用关键字virtual,使得Worker被用作Singer和Waiter的虚基类(public和virtual的顺序无关紧要)class Singer : virtual public Worker {...}; class Waiter : public virtual Worker {...}; //然后可将SingingWaiter类定义为: class SingingWaiter : public Singer, public Waiter{...}; // SingingWaiter对象将只包含Worker对象的一个副本【Singer和Waiter对象共享一个Worker对象】 class A{int a; public: A(int n=0):a{n}{}...}; class B:public A{int b; public: B(int m=0,int n=0):A{n},b(m){}...}; class C:public B{int c; public: C(int q=0,int m=0,int n=0):B{m,n},c(q){}...}; // C的构造函数只能调用B的构造函数(传递m,n给B的构造,使用值q),B只能调用A的构造函数(传递n给A的构造,使用值m)如果Woker是虚基类,上面这种信息自动传递将不起作用,对于以下代码:
SingingWaiter(const Worker& wk, int p=0, int v=Singer::other) : Waiter(wk,p), Singer(wk, v){} // 存在缺陷,自动传递信息,通过两条不同途径将wk传递给Worker对象为了避免上述代码存在的冲突,C++的虚基类禁止信息通过中间类自动传递给基类,需要显式地调用所需的基类构造函数:
SingingWaiter(const Worker& wk, int p=0, int v=Singer::other) : Worker(wk), Waiter(wk,p), Singer(wk, v){} // 对于虚基类必须这样做;对于非虚基类,则是非法的假设没有在SingingWaiter中重定义show()方法,并试图用SingingWaiter对象调用继承的Show()方法:
SingingWaiter newhire(有参构造); newhire.Show(); // 多重继承,每个直接祖先都有Show(),因此存二义性(单继承使用最近祖先中的定义)可以使用作用域解析运算符:
newhire.Singer::Show(); // 使用 Singer 中定义的更好的仿射是在SingingWaiter中重新定义Show(),并指出要使用哪个Show()。比如:
void SingingWaiter::Show(){ Singer::Show(); } -
创建和使用类模板
使用template <class Type>或者template <typename Type>定义类模板,class是变量类型名,Type是变量名称。模板须与特定的模板实例化请求一起使用
代码示例(写在头文件中):template <class Type> class Stack{ private: enum {MAX=10}; Type items[MAX]; int top; public: Stack(); bool isempty(); bool isfull(); bool push(const Type& item); bool pop(Type& item); }; template <class Type> Stcak<Type>::Stack(){ top=0; } template <class Type> bool Stack<Type>::isempty(){ return top==0; } template <class Type> bool Stack<type>::isfull(){ return top==MAX; } template <class Type> bool Stack<Type>::push(const Type& item){ if(top<MAX){ items[top++] = item; return true; } else return false; } template <class Type> bool Stack<Type>::pop(Type& item){ if(top>0){ item = items[--top]; return true; } else return false; } Stack<int> s1; // create a stack of ints Stack<string> s2; // create a stack of string objects泛型标识符(这里的Type称为类型参数,类似于变量,但赋给它们的只能是类型)
template <class T> void simple(T t){ cout<<t<<'\n'; } // 函数模板 ... simple(2); // generate void simple(int) simple("two"); // generate void simple(const char *) -
指针栈
- 不正确使用
Stack<char *> st; // create a stack for pointers-to-char
version 1:string po; 替换为char * po;【旨在用char指针而不是string对象接受键盘输入,但仅仅创建指针而没有创建用于保存输入字符串的空间】
version 2:string po; 替换为char po[40];【为输入字符串分配了空间,po的类型为char *,可以存在指针栈中。但是对于出栈代码:item = items[--top];引用变量item须引用某种类型的左值而非数组名,即使item能引用数组,也不能为数组名赋值】
version 3:string po; 替换为char* po = new char[40];【po是变量,与pop()代码兼容。然鹅,只有一个pop变量,该变量总是指向相同的内存单元(虽然每当读取到新字符串是内存地址发生改变,但每次push(),加到栈的地址相同)】 - 正确使用
让调用程序提供一个指针数组,其中每个指针都指向不同的字符串。【创建不同指针是调用程序的职责,而不是栈的职责(管理指针)】
- 不正确使用
-
数组模板示例和非类型参数
模板常用于容器类(类型参数的概念非常适合于将相同的存储方案用于不同的类型)template <class T, int n> // class(或typename)指出T为类型参数,int指出n的类型,这种参数(指定特殊的类型而不是用作泛型名)称为非类型(non-type)或表达式参数 class ArrayTP{ private: T ar[n]; ...... }; ArrayTP<T, n>::ArrayTP(const T& v){ for(int i=0;i<n;i++) ar[i]=v; } ... ArrayTP<double, 12> eggweights; // 编译器定义名为ArrayTP<double, 12>的类。创建一个该类型的对象
模板类可以用作基类、组件类,还可以用作其它模板的类型参数
Stack< Stack<int> > ssi; // an stack of stacks of int
对于上面定义的ArrayTP类:ArrayTP< ArrayTP<int,5>, 10 > twodee; // 等价于 int twodee[10][5];
模板可以包含多个类型参数(假设希望类可以保存两种值,创建并使用Pair模板)
-
模板的具体化
- 隐式实例化
上面使用的诸如:ArrayTP<double, 12> stuff;
编译器在需要对象之前,不会生成类的隐式实例化:ArrayTP<double, 30> * pt; // a pointer, no object needed yet pt = new ArrayTP<double, 30>; // now an object is needed - 显式实例化
template class ArrayTP<string, 100>; // 使用template并指出所需类型来声明类时 - 显式具体化
是特定类型(用于替换模板中的泛型)的定义。有时可能需要为特殊类型实例化是对模板进行修改
比如定义一个数组排序类,数字类型可以正常比较排序、对象类型可以通过定义T::operator>()方法也可行;但T如果是由const char *表示的字符串,不可行。
模板可以正常工作,但字符串将按地址(按字母顺序)排序,须定义使用strcmp(),而不是>来对值进行比较。这种情况提供为具体类型定义的模板,而不是为泛型定义的模板
定义:template <> class SortedArray<const char *>{...}; template <typename T> class SortedArray{...}; // 普通模板定义 - 部分具体化
//模板用作参数 template <template <typename T> class Thing> // 模板参数是template <typename T> class Thing,其中template <typename T> class是类型,Thing是参数 class Crab{...}; //假设有以下声明: Crab<King> legs; // 模板参数King必须是一个模板类,其声明与模板参数Thing的声明相匹配 template <typename T> class King{...};
- 隐式实例化
-
模板类和友元
- 非模板友元
template <class T> class HasF{ public: friend void counts(); // 是所有HasF实例化的友元(不通过对象调用) ... }; - 约束(bound)模板友元【友元的类型取决于类被实例化时的类型】
首先在类定义前声明模板函数:template <typename T> void counts(); template <typename T> void report(T &); //然后在函数中再次将模板声明为友元: template <typename TT> class HasF{ public: friend void counts<TT>(); friend void report<>(HassF<TT> &); // <>可以为空,等价于report< HassF<TT> >(HassF<TT> &); }; - 非约束(unbound)模板友元【友元的所有具体化都是类的每一个具体化的友元】
template <class T> class ManyF{ public: template <typename C, typename D> friend void show(C &, D &); ... };
- 非模板友元
#include <iostream>
#include <string>
#include <valarray>
using namespace std;
class Student {
private:
typedef valarray<double> ArrayDb;
string name; // string对象——name
ArrayDb scores; // valarray<double>对象——scores
ostream& arr_out(ostream& os) const; // private method for scores output, 没有valarray的<<实现
public:
Student() : name("none student"), scores() {} // 默认构造函数,使用成员初始化列表
explicit Student(const string& s) : name(s), scores() {} // explict修饰构造函数(单个参数),用于限制隐式转换和复制初始化,只能以显式方式进行类型转换
explicit Student(int n) : name("none"), scores(n) {} // n代表数组scores的元素个数
Student(const string& s, int n) : name(s), scores(n) {}
/*
如果不使用explicit
Student doh("moon", 10); // name设置为“moon”,create array of 10 elements
doh = 5; // 将隐式转换,调用构造函数 Student(int n){} name设置为“none”,create array of 5 elements
若是使用explict,上面这行代码编译错误
【C++的约束和限制】注意:使用explict防止单参数构造函数的隐式转换,使用const限制方法修改数据。根本原因——编译阶段出现的错误优于在运行阶段出现的错误
*/
Student(const string& s, ArrayDb & a) : name(s), scores(a) {}
Student(const char* str, const double* pd, int n) : name(str), scores(pd, n) {} // scores(pd, n)调用构造函数ArrayDb(const double*, int)
~Student() {}
double Average() const;
const string& Name() const;
double& operator[](int i);
double operator[](int i) const;
// 友元函数[非成员函数,却有访问类内部成员的权限] 输入输出
friend istream& operator>>(istream& is, Student& stu); // input 1 word
friend istream& getline(istream& is, Student& stu); // input 1 line
friend ostream& operator<<(ostream& os, const Student& stu);
};
double Student::Average() const {
if (scores.size() > 0) return scores.sum() / scores.size();
else return 0;
}
/*对于私有继承,Average方法改写如下:
double Student::Average() const {
if (ArrayDb::size() > 0) return ArrayDb::sum() / ArrayDb::size();
else return 0;
}
--使用包含时将使用对象名调用方法,使用私有继承时将使用类名和作用域解析运算符调用方法--
*/
const string& Student::Name() const { return name; }
double& Student::operator[](int i) { return scores[i]; }
/*对于私有继承
double& Student::operator[](int i) { return ArrayDb::operator[](i); }
*/
double Student::operator[](int i) const { return scores[i]; }
ostream& Student::arr_out(ostream& os) const {
int i;
int lim = scores.size(); // 私有继承: int lim = ArrayDb::size();
if (lim > 0) {
for (i = 0; i < lim; i++) {
os << scores[i] << " "; // 私有继承: ArrayDb::operator[](i)
if (i % 5 == 4) os << endl;
}
if (i % 5 != 0) os << endl;
}
else os << " empty array";
return os;
}
// friend函数
istream& operator>>(istream& is, Student& stu) {
is >> stu.name; // 私有继承: is>>(string &)stu;
return is;
}
istream& getline(istream& is, Student& stu) {
getline(is, stu.name); // 私有继承: getline(is, (string &)stu);
return is;
}
ostream& operator<<(ostream& os, const Student& stu) {
os << "Scores for " << stu.name << ":\n";
// os << "Scores for " << (const string&)stu << ":\n"; // 访问基类的友元,显式地转换为基类调用正确的函数
stu.arr_out(os); // use private method
return os;
}
//测试Student的代码
void set(Student& sa, int n);
int sixth_main() {
const int pupils = 3;
const int quizzes = 5;
Student ada[pupils] = { Student(quizzes), Student(quizzes), Student(quizzes) };
int i;
for (i = 0; i < pupils; i++) set(ada[i], quizzes);
cout << "\nStudent List:\n";
for (i = 0; i < pupils; i++) cout << ada[i].Name() << endl;
cout << "\n Results:";
for (i = 0; i < pupils; i++) {
cout << endl << ada[i];
cout << "average: " << ada[i].Average() << endl;
}
cout << "Done!\n";
return 0;
}
void set(Student& sa, int n) {
cout << "please enter the student's name: ";
getline(cin, sa);
cout << "please enter " << n << "quiz scores:\n";
for (int i = 0; i < n; i++) cin >> sa[i];
while (cin.get() != '\n') continue;
}
/*Pair*/
template<class T1, class T2>
class Pair { // 类模板的另一项新特性是,可以为类型参数提供默认值。若template<class T1, class T2=int> class Pair Pair<double, double> m1;//可行 Pair<double> m2;//T2默认为int
private:
T1 a;
T2 b;
public:
T1& first();
T2& second();
T1 first() const { return a; }
T1 second() const { return b; }
Pair(const T1& aval, const T2& bval) : a(aval), b(bval) {}
Pair() {}
};
template<class T1, class T2>
T1& Pair<T1, T2>::first() { return a; }
template<class T1, class T2>
T2& Pair<T1, T2>::second() { return b; }
int sixth_main_2() {
Pair<string, int> rating[4] = {
Pair<string, int>("The first", 5),
Pair<string, int>("The second", 4),
Pair<string, int>("The third", 3),
Pair<string, int>("The fourth", 2),
};
int joints = sizeof(rating) / sizeof(Pair<string, int>);
cout << "Rating:\t rank\n";
for (int i = 0; i < joints; i++) cout << rating[i].second() << ":\t " << rating[i].first() << endl;
cout << "Oops! Revised ratingL\n";
rating[3].first() = "modified string";
rating[3].second() = 99;
cout << rating[3].second() << ":\t " << rating[3].first() << endl;
return 0;
}
/*成员模板*/
template <typename T>
class beta {
private:
template <typename V> // 嵌套的成员模板
class hold {
private:
V val;
public:
hold(V v=0) : val(v){}
void Show() const { cout << val << endl; }
V Value() const { return val; }
};
hold<T> q; // 模板对象
hold<int> n;
public:
beta(T t, int i) : q(t) ,n(i){}
template<typename U> // template method
U blab(U u, T t) { return (n.Value() + q.Value()) * u / t; }
void Show() const { q.Show(); n.Show(); }
};
int main() {
beta<double> guy(3.5, 3); // T为double
guy.Show();
cout << "V was set to T, which is double, then V was set to int\n";
cout << guy.blab(10, 1.2) << endl;
cout << "U was set to int\n";
cout << guy.blab(10.0, 1.2) << endl;
cout << "U was set to double\n";
return 0;
}
7 友元、异常和其它
- 友元类
友元类的所有方法都可以访问原始类的私有成员和保护成员。【或者可以将特定的成员函数指定为另一个类的友元,做严格限制】
什么情况下希望一个类成为另一个类的友元?
编写一个模拟电视机(Tv类)和遥控器(Remote类)的程序,它们直接的关系并非公有继承的is-a关系。也不是包含或私有继承的has-a关系。
遥控器(Remote)可以改变电视机(Tv)的状态,表示应将Remote作为Tv的一个友元
friend class Remote; // Remote成为友元类 【类友元是一种自然用语,用于表示一些关系】
友元声明可以位于公有、私有或保护部分,位置不重要。
由于Remote类提到了Tv类,编译器须了解Tv类后才能处理Remote类【先定义Tv类;也可以使用前向声明(forward declaration)】
Tv类与Remote类成为彼此的友元(两者之间互相影响:Tv使Remote蜂鸣)
class Tv{
friend class Remote;
public:
void buzz(Remote & r);
...
};
class Remote{
friend class Tv;
public:
void Bool volup(Tv & t){t.volup();} // 由于Remote声明在Tv之后
...
};
inline void Tv::buzz(Remote & r){...}
// 该方法须在Tv声明的外部定义,位于Remote声明之后【如果不想inline,应在一个单独的方法定义文件在定义】
- 共同的友元
函数需要访问两个类的私有成员。从逻辑上看,这样的函数应是每个类的成员函数(不可能)。
可以是一个类的成员,是另一个类的友元;但是有时作为两个类的友元更合适
例如:有一个Probe类(可编程的测量设备)和Analyzer类(可编程分析设备),两个类都有内部时钟,且希望它们同步
class Analyzer; // forward declaration
class Probe{
friend void sync(Analyzer & a, const Probe & p); // sync a to p
friend void sync(Probe & p, const Analyzer & a); // sync p to a
...
};
class Analyzer{
friend void sync(Analyzer & a, const Probe & p); // sync a to p
friend void sync(Probe & p, const Analyzer & a); // sync p to a
...
};
//define the friend function
inline void sync(Analyzer & a, const Probe & p){ ... }
inline void sync(Probe & p, const Analyzer & a){ ... }
- 嵌套类
包含类的成员函数可以创建和使用被嵌套类的对象;而仅当声明位于公有部分,才能在包含类的外面使用嵌套类,且须使用作用域解析运算符
对类的包含与嵌套是不同的:包含意味着将类对象作为另一个类的成员,而类嵌套不创建类成员,就是定义了一种类型,该类型仅在包含嵌套类声明的类中有效
【类嵌套通常是为例帮助实现另一个类,避免命名冲突】
class Queue{
// Queue嵌套了结构定义(变相嵌套类)——结构是一种其成员在默认情况下为公有的类
private:
// class scopr definitions
struct Node {Item item; struct Node * next;}; // Node is a nested stucture definition local to this class
...
};
// 以上的定义没有显式构造函数,找到创建Node对象的位置:
bool Queue::enqueue(const Item & item){
if(iffull()) return false;
try{
Node * add = new Node; // on failure, new throws std::bad_alloc exception // new 导致的内存分配问题
}
catch(bad_alloc & ba){
cout << ba.what() << endl;
exit(EXIT_FAILURE); // #include <cstdlib>
}
add->item = item;
add->next = NULL;
...
}
//更合适的类定义:
class Queue{
class Node{
public:
Item item;
Node * next;
Node(const Item & i) : item(i), next(0){}
};
...
};
//重新编写enqueue()【代码更短、也更安全】:
bool Queue::enqueue(const Item & item){
if(iffull()) return false;
Node * add = new Node(item);
// on failure, new throws std::bad_alloc exception
...
}
//定义Node的构造函数:
Queue::Node::Node(const Item & i) : item(i), next(0){}
注意:类声明位置决定了类的作用域或可见性。类可见后,访问控制规则(公有、保护、私有、友元)将决定程序对嵌套成员类成员的访问权限
- 引发异常、try块和catch块
假设对于表达式:2.0 * x * y / (x + y),其中y是x的负值,则表达式出现被零除,新式编译器生成一个表示无穷大的特殊浮点数处理,更为常见地是导致程序崩溃
对于这种问题,有多种处理方法:- 调用
abort()函数【原型位于cstdlib(或stdlib.h)中】,其典型实现是向标准错误流(cerr)发送消息abnormal program termination,然后终止程序,返回一个随实现而异的值。
abort()是否刷新文件缓冲区(用于存储读写到文件中的数据的内存区域)取决于实现。若愿意,可以使用exit()——刷新文件缓存区,不显示消息
- 调用
double use_abort(double a, double b){
if(a==-b){
std::cout<<"untenable arguments to use_abort()\n"
std::abort();
}
return 2.0 * a * b / (a + b);
}
// 返回错误码【使用函数的返回值来指出问题】
bool use_code(double a, double b, double * ans){
if(a == -b){
*ans = DBL_MAX; // #include <cfloat> for DBL_MAX
return false;
}else{
*ans = 2.0 * a * b / (a + b);
return true;
}
}
- 异常处理机制【若引发了异常却没有try块或匹配的处理程序时,默认使用abort()函数】
int main(){
double x,y,z;
cout<<"Enter two number: ";
while(cin>>x>>y){
try{
if(x == -y) throw "bad arguments: x = -y not allowed!"; // 被引发的异常是字符串【也可以是其它C++类型,通常是类类型】
z = 2.0 * x * y / (x + y);
}catch(const char * s){ // 捕获异常【字符串】并作异常处理
cout << s << endl;
cout << "Enter a new pair of numbers: ";
continue;
}
// 输出结果
}
return 0;
}
// 将对象用作异常类型,可以使用不同的异常类型区分不同函数在不同情况下引发的异常
class use_throw{
private:
double v1; double v2;
public:
use_throw(int a=0, int b=0) : v1(a), v2(b){}
void msg();
};
inline void use_throw::msg(){
cout << "use_throw(" << v1 << ", " << v2 <<"): " << "invalid arguments: a = -b \n";
}
// 则try catch块改写成
try{
if(x == -y) throw use_throw(x, y);
z = 2.0 * x * y / (x + y);
}catch(use_throw & ut){
ut.msg();
cout << "try again!"; continue;
}
- 异常规范
double harm(double a) throw(bad_thing); // may throw bad_thing exception
double marm(double) throw(); // doesn't throw an exception
double marm(double) noexcept; // 使用关键字noexcept指出函数不会引发异常
noexcept()判断其操作数是否会引发异常
-
栈解退(unwinding the stack)
try块没有直接调用引发异常的函数,而是调用了对引发异常的函数进行调用的函数,程序流程将从引发异常的函数跳到包含try块和处理程序的函数 -
迷失方向的异常
意外异常(unexpected exception)——在异常规范的函数中引发的,须与规范列表中的某种异常匹配
未捕获异常(uncaught exception)——没有try块或没有匹配的catch块(不会造成程序立刻异常终止,程序先调用terminate(),terminate()调用abort()) -
异常类
C++异常的主要目的是为设计容错程序提供语言级支持,使得在程序设计中包含错误处理功能更容易- 头文件(exception.h或except.h)定义了exception类,C++可以把它用作其它异常类的基类(有一个what()的虚方法,可以在exception的派生类中重定义它)
- 头文件stdexcept定义了其他几个异常类——logic_error类和runtime_error类…【公有方式从exception派生而来】
运行阶段类型识别(Runtime Type Idetification,RTTI)
假设有一个类层次结构,其中的类都是从同一个基类派生而来的,则可以让基类指针指向其中任何一个类的对象。
这样便可以调用这样的函数:在处理一些信息后,选择一个类,并创建这种类型的对象,然后返回它的地址,而该地址可以被赋给基类指针。如何知道指针指向的是哪种对象呢?
为何要知道类型?可能希望调用类方法的正确版本,在这种情况下,只要该函数是类层次结构中所有成员都拥有的虚函数,则并不真正需要知道对象的类型。
但派生对象可能包含不是继承而来的方法,在这种情况下,只有某些类型的对象可以使用该方法。也可能是出于调试目的,想跟踪生成的对象的类型。对于后两种情况,RTTI提供解决方案。
- 如果可能的话,dynamic_cast运算符将使用一个指向基类的指针来生成一个指向派生类的指针;否则,该运算符返回0——空指针。
dynamic_cast运算符是最常用的RTTI组件,它不能回答“指针指向的是哪类对象”这样的问题,但能够回答“是否可以安全地将对象的地址赋给特定类型的指针”这样的问题。
class Grand {// has virtual methods };
class superb : public Grand { ... };
class Magnificent : public Superb { ...};
假设有下面的指针:
Grand * pg = new Grand;
Grand * ps = new Superb;
Grand * pm = new Magnificent;
对于下面的类型转换:
Magnificent * p1 = (Magnificent * ) pm;// #1 安全
Magnificent * p2 =(Magnificent *) pg;//#2 不安全 将基数对象(Grand)的地址赋给派生类(Magnificent)指针。因此,程序将期望基类对象有派生类的特征
Superb * p3 = (Magnificent *) pm;//#3 安全 将派生对象的地址赋给基类指针。即公有派生确保 Magnificent对象同时也是一个Superb对象(直接基类)和一个Grand对象(间接基类)。
// 虚函数确保了将这3种指针中的任何一种指向Magnificent对象时,都将调用Magnificent方法。
dynamic_cast 的语法。该运算符的用法如下,其中pg 指向一个对象:
Superb * pm = dynamic_cast<Superb * >(pg) ;
这提出了这样的问题:指针pg的类型是否可被安全地转换为Superb*?如果可以,运算符将返回对象的地址,否则返回一个空指针。
- typeid运算符返回一个指出对象的类型的值。
typeid运算符使得能够确定两个对象是否为同种类型。它与sizeof有些相像,可以接受两种参数:
- 类名;
- 结果为对象的表达式。
typeid运算符返回一个对type_info对象的引用,其中 type_info是在头文件typeinfo(以前为typeinfo.h)中定义的一个类。
type_info类重载了==和!=运算符,以便可以使用这些运算符来对类型进行比较。
例如,如果pg 指向的是一个 Magnificent对象,则下述表达式的结果为bool值 true,否则为false:
typeid(Magnificent) == typeid(*pg)
如果pg是一个空指针,程序将引发bad_typeid异常。该异常类型是从exception类派生而来的,是在头文件typeinfo中声明的。
type_info类的实现随厂商而异,但包含一个name()成员,该函数返回一个随实现而异的字符串:通常(但并非一定)是类的名称。
例如,下面的语句显示指针pg 指向的对象所属的类定义的字符串:
cout << "Now processing type " << typeid(*pg).name() << ".\n";
- type_info 结构存储了有关特定类型的信息。
【只能将RTTI用于包含虚函数的类层次结构,原因在于只有对于这种类层次结构,才应该将派生对象的地址赋给基类指针。】
stastic_cast、const_cast和reiterpret_cast
在C++的创始人Bjarne Stroustrup看来,C语言中的类型转换运算符太过松散。例如下面的代码:
struct Data{ double data [200]; };
struct Junk{ int junk [100]; };
Data d = { 2.5e33,3.5e-19,20.2e32 };
char * pch = (char *) (&d); // type cast #1 - convert to string
char ch = char (&d); // type cast #2 - convert address to a char
Junk * pj = (Junk *) (&d); // type cast #3 - convert to Junk pointer
首先,上述3种类型转换中没有一个是有意义的。其次,这3种类型转换中在C语言中都是允许的。
对于这种松散情况,Stroustrop采取的措施是,更严格地限制允许的类型转换,并添加4个类型转换运算符,使转换过程更规范:
- dynamic_cast【上面介绍过,该运算符的用途是,使得能够在类层次结构中进行向上转换(由于is-a关系,这样的类型转换是安全的),而不允许其他转换。】
- const_cast
用于执行只有一种用途的类型转换,即改变值为const或 volatile
如果类型的其他方面也被修改,则上述类型转换将出错。也就是说,除了const或volatile特征(有或无)可以不同外,type_name和 expression 的类型必须相同。
假设High和Low是两个类:
High bar;
const High * pbar = &bar;
High * pb = const_cast<High *>(pbar); // valid
const Low * pl = const_cast<const Low *>s (pbar); // invalid
第一个类型转换使得*pb成为一个可用于修改 bar对象值的指针,它删除const 标签。第二个类型转换是非法的,因为它同时尝试将类型从const High*改为const Low *。
- static_cast
假设High是Low的基类,而Pond是一个无关的类,从 Low到Pond的转换是不允许的,High到Low的转换、从Low到High 的转换都是合法的:
High bar;
Low blow;
...
High * pb = static_cast<High *> (&blow); // valid upcast
Low * pl = static_cast<Low *>(&bar); // valid downcast
Pond * pmer = static_cast<Pond *> (&blow); // invalid,Pond unrelated
- reinterpret_cast【通常,这样的转换适用于依赖于实现的底层编程技术,是不可移植的。】
用于天生危险的类型转换。它不允许删除const,但会执行其他令人生厌的操作。有时程序员必须做一些依赖于实现的、令人生厌的操作,使用reinterpret_cast
运算符可以简化对这种行为的跟踪工作。该运算符的语法与另外3个相同。
下面是一个使用示例:
struct dat {short a; short b;};
long value = 0xA224B118;
dat * pd = reinterpret_cast< dat*> ( &value) ;
cout << hex << pd->a; // display first 2 bytes of value
tv.h
#ifndef TV_H_
#define TV_H_
class Tv
{
public:
friend class Remote; //Remote can access Tv privcate parts
enum { Off, On };
enum { MinVal, MaxVal = 20 };
enum { Antenna, Cable };
enum { TV, DVD };
Tv(int s = Off, int mc = 125) :state(s), volume(5), maxchannel(mc), channel(2), mode(Cable), input(TV) {}
void onoff() { state = (state == On) ? Off : On; }
bool ison() const { return state == On; }
bool volup();
bool voldown();
void chanup();
void chandown();
void set_mode() { mode = (mode == Antenna) ? Cable : Antenna; }
void set_input() { input = (input == TV) ? DVD : TV; }
void setting() const; // display all settings
/*
friend void Remote::set_chan(Tv & t, int c); // 然而编译器要处理这条语句需要先知道Remote的定义
//(应将Remote的定义方法Tv定义之前,但是Remote的方法使用了Tv对象,意味着Tv定义应在Remote之前)。避开这种循环依赖的方法是:使用前向声明
声明顺序如下:
class Tv; // 前向声明
class Remote{...}; // Tv-using methods 仅仅用作原型
class Tv{...}
*/
private:
int state; // On or Off
int volume;
int maxchannel; // maximum number of channels
int channel;
int mode; // Antenna or Cable
int input; // TV or DVD
};
class Remote {
private:
int mode; // controls TV or DVD
public:
Remote(int m=Tv::TV) : mode(m){}
// 以下所有类方法都将一个Tv对象引用作为参数(Remote须针对特定Tv)
// 多数Remote方法都是调用Tv类的公有接口实现(下面的例子中除了set_chan()方法都是)
bool volup(Tv& t) { return t.volup(); }
bool voldown(Tv& t) { return t.voldown(); }
void onoff(Tv& t) { t.onoff(); }
void chanup(Tv& t) { return t.chanup(); }
void chandown(Tv& t) { return t.chandown(); }
void set_chan(Tv& t, int c) { t.channel = c; } // 可以在Tv类中将该方法声明为友元函数,从而不必将整个Remote类声明为友元类
void set_mode(Tv& t) { t.set_mode(); }
void set_input(Tv& t) { t.set_input(); }
};
#endif // !TV_H_
tvfm.h
#pragma once // 只要在头文件的最开始加入这条预处理指令,就能够保证头文件只被编译一次
#ifndef TVFM_H_
#define TVFM_H_
// 对tv.h的前向声明和友元函数改进
class Tv; // forward declaration
class Remote {
private:
int mode; // controls TV or DVD
public:
enum{ Off, On };
enum{ MinVal, MaxVal = 20 };
enum{ Antenna, Cable };
enum{ TV, DVD };
Remote(int m = TV) : mode(m) {}
// 以下所有类方法都仅仅声明原型
bool volup(Tv& t);
bool voldown(Tv& t);
void onoff(Tv& t);
void chanup(Tv& t);
void chandown(Tv& t);
void set_chan(Tv& t, int c);
void set_mode(Tv& t);
void set_input(Tv& t);
};
class Tv
{
public:
friend void Remote::set_chan(Tv & t, int c); // 友元函数
enum{ Off, On };
enum{ MinVal, MaxVal = 20 };
enum{ Antenna, Cable };
enum{ TV, DVD };
Tv(int s = Off, int mc = 125) :state(s), volume(5), maxchannel(mc), channel(2), mode(Cable), input(TV) {}
void onoff() { state = (state == On) ? Off : On; }
bool ison() const { return state == On; }
bool volup();
bool voldown();
void chanup();
void chandown();
void set_mode() { mode = (mode == Antenna) ? Cable : Antenna; }
void set_input() { input = (input == TV) ? DVD : TV; }
void setting() const; // display all settings
private:
int state; // On or Off
int volume;
int maxchannel; // maximum number of channels
int channel;
int mode; // Antenna or Cable
int input; // TV or DVD
};
// Remote methods as inline function
inline bool Remote::volup(Tv& t) { return t.volup(); }
inline bool Remote::voldown(Tv& t) { return t.voldown(); }
inline void Remote::onoff(Tv& t) { t.onoff(); }
inline void Remote::chanup(Tv& t) { return t.chanup(); }
inline void Remote::chandown(Tv& t) { return t.chandown(); }
inline void Remote::set_chan(Tv& t, int c) { t.channel = c; } // 在Tv类中将该方法声明为友元函数
inline void Remote::set_mode(Tv& t) { t.set_mode(); }
inline void Remote::set_input(Tv& t) { t.set_input(); }
#endif // !TV_H_
主程序:
#include <iostream>
#include <cstring>
//#include "tv.h"
#include "tvfm.h"
using namespace std;
bool Tv::volup() {
if (volume < MaxVal) {
volume++;
return true;
}
else return false;
}
bool Tv::voldown() {
if (volume > MinVal) {
volume--;
return true;
}
else return false;
}
void Tv::chanup() {
if (channel < maxchannel) channel++; // 1到maxchannel循环
else channel = 1;
}
void Tv::chandown() {
if (channel > 1) channel--;
else channel = maxchannel;
}
void Tv::setting() const{
cout << "Tv is " << (state == Off ? "Off" : "On") << endl;
if (state == On) {
cout << "Volume setting = " << volume << endl;
cout << "Channel setting = " << channel << endl;
cout << "Mode = " << (mode == Antenna ? "Antenna" : "Cable") << endl;
cout << "Input = " << (input == TV ? "TV" : "DVD") << endl;
}
}
int seven_main() {
Tv s42;
cout << "inital settings for s42\" TV:\n";
s42.setting(); // 当前为 OFF
s42.onoff(); // 打开 ON
s42.chanup();
cout<<"adjusted settings for s42\" TV:\n";
s42.chanup(); // 两次channel up 从2变为4
cout << "adjusted settings for s42\" TV:\n";
s42.setting();
Remote r;
r.set_chan(s42, 10);
r.volup(s42);
r.volup(s42);
cout << "\n s42 settings after using remote:\n";
s42.setting();
Tv s58(Tv::On);
s58.set_mode();
r.set_chan(s58, 28);
cout << "\n s58 settings after using remote:\n";
s58.setting();
return 0;
}
8 string类和标准模板库
-
标准C++ string类
<cstring>
(注意,头文件 string.h和cstring支持对C-风格字符串进行操纵的C库字符串函数,但不支持 string类)。
要使用类,关键在于知道它的公有接口,而string类包含大量的方法,其中包括了若干构造函数,用于将字符串赋给变量、合并字符串、比较字符串和访问各个元素的重载运算符以及用于在字符串中查找字符和子字符串的工具等。
string实际上是模板具体化basic_string<char>的一个typedef,同时省略了与内存管理相关的参数open()方法要求使用一个C-风格字符串作为参数,c_str()方法返回一个指向C-风格字符串(与用于调用c_str()方法的string对象相同)的指针
string filename; ofstream fout;
fout.open(filename.c_str()); -
模板basic_string有4个具体化:
typedef basic_string<char> string;
typedef basic_string<wchar_t> wstring;
typedef basic_string<char16_t> u16string; // C++11
typedef basic_string<char32_t> u32string;
模板auto_ptr、unique_ptr和shared_ptr【智能指针是行为类似于指针的类对象(还有其它功能)】
void remodel(string & str){
string * ps = new string(str);
...
str = *ps;
// 解决方案: delete ps;
return;
}
上面的函数存在缺陷,每当调用时,该函数都分配堆中的内存,但从不收回,从而导致内存泄漏。但是存在以下变体:
void remodel(string & str){
string * ps = new string(str);
...
if(weird_thing()) throw exception(); // 出现异常时,delete将不被执行,因此也将导致内存泄漏
str = *ps;
delete ps;
return;
}
ps的问题在于,它只是一个常规指针,不是有析构函数的类对象。如果他是对象,则可以在对象过期时,让它的析构函数删除指向的内存【这正是auto_ptr(C98)、unique_ptr和shared_ptr背后的思想】
三个指针指针模板都定义了类似指针的对象,可以将new获得(直接或间接)的地址赋给这种对象。当指针指针过期时,其析构函数将使用delete来释放内存
要创建指针指针对象,须包含头文件 memeory ,该文件模板定义。模板auto_ptr包含如下构造函数:
template<class X>
class auto_ptr{
public:
explict auto_ptr(X * p=0) throw(); // throw()意味着构造函数不会引发异常【将被摒弃】,explict表示不能将指针自动转换(隐式)为智能指针对象
...
};
auto_ptr<double> pd(new double);
// 创建一个指向double类型的auto_ptr 【使用上同 double * pd】
上面例子中的代码改写如下:
#include <memory>
void remodel(string & str){
std::auto_ptr<string> ps(new string(str));
...
if(weird_thing()) throw exception();
str = *ps;
// delete ps; 这条语句已经不再需要了
return;
}
注意智能指针使用有几个点:
shared_ptr<double> pd;
double* p_rg = new double;
pd = p_reg; // 不允许
pd = shared_ptr<double>(p_reg); // 允许 显式(强制)转换
shared_ptr<double> pshared(p_reg); // 允许
string vstr("I wanne play.");
shared_ptr<string> pvstr(&vstr); // 不允许,在pvstr过期时,程序将吧delete运算符用于非堆内容
为什么要摒弃auto_ptr?
auto_ptr<string> ps(new string("this is test."));
auto_ptr<string> vostr; vostr = ps;
// 两个指针指向同一个string对象,程序会试图删除同一个对象两次【不被允许】
应使用哪种智能指针呢?
- 如果程序要使用多个指向同一个对象的指针,应选择
shared_ptr。这样的情况包括:有一个指针数组,并使用一些辅助指针来标识特定的元素,如最大的元素和最小的元素;两个对象包含都指向第三个对象的指针;STL容器包含指针。- 很多STL算法都支持复制和赋值操作,这些操作可用于shared ptr,但不能用于unique ptr(编译器发出警告)和 auto ptr(行为不确定)。如果您的编译器没有提供shared_ptr,可使用Boost库提供的shared_ptr。
- 如果程序不需要多个指向同一个对象的指针,则可使用
unique_ptr。如果函数使用new分配内存,并返回指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。这样,所有权将转让给接受返回值的unique ptr,而该智能指针将负责调用delete。- 可将unique_ptr存储到STL容器中,只要不调用将一个unique_ptr复制或赋给另一个的方法或算法(如 sort())。
-
标准模板库(STL)
STL提供了一组表示容器、迭代器、函数对象和算法的模板。- 容器是一个与数组类似的单元,可以存储若干个值。STL容器是同质的,即存储的值的类型相同;
- 算法是完成特定任务(如对数组进行排序或在链表中查找特定值)的处方;
- 迭代器能够用来遍历容器的对象,与能够遍历数组的指针类似,是广义指针;
- 函数对象是类似于函数的对象,可以是类对象或函数指针(包括函数名,因为函数名被用作指针)。
STL使得能够构造各种容器(包括数组、队列和链表)和执行各种操作(包括搜索、排序和随机排列)。
分配器:
与string类相似,各种STL容器模板都接受一个可选的模板参数,该参数指定使用哪个分配器对象来管理内存。例如,vector模板的开头与下面类似:
template <class T, class Allocator = allocator<T>>
class vector {... }
如果省略该模板参数的值,则容器模板将默认使用allocator<T>类。这个类使用new和delete。 -
容器类
STL具有容器概念和容器类型。概念是具有名称(如容器、序列容器、关联容器等)的通用类别;容器类型是可用于创建具体容器对象的模板。
以前的11个容器类型分别是deque、list、queue、priority_queue、stack、vector、map、multimap、set、multiset和 bitset(不讨论bitset,它是在比特级处理数据的容器);C++11新增了forward_ list、unordered_map、unordered multimap、unordered_set和 unordered_multiset,且不将bitset视为容器,而将其视为一种独立的类别。
-
容器概念指定了所有STL容器类都必须满足的一系列要求。
-
C++11新增了一些容器要求
-
可以通过添加要求来改进基本的容器概念。序列( sequence)是一种重要的改进,因为7种STL容器类型都是序列
-
关联容器(associative container)是对容器概念的另一个改进【树型结构实现】。关联容器将值与键关联在一起,并使用键来查找值。
例如,值可以是表示雇员信息(如姓名、地址、办公室号码、家庭电话和工作电话、健康计划等)的结构,而键可以是唯一的员工编号。
为获取雇员信息,程序将使用键查找雇员结构。对于容器X,表达式X:value_type通常指出了存储在容器中的值类型。对于关联容器来说,表达式X:key_type指出了键的类型。
关联容器的优点在于:提供了对元素的快速访问。与序列相似,关联容器也允许插入新元素,但不能指定元素的插入位置。原因是关联容器通常有用于确定数据放置位置的算法,以便能够快速检索信息。STL 提供了4种关联容器: set、multiset、map和 multimap。前两种是在头文件set (以前分别为
set.h和multiset.h)中定义的,而后两种是在头文件map(以前分别为map.h和multimap.h)中定义的。
-
-
泛型编程:
STL是一种泛型编程(generic programming)。
面向对象编程关注的是编程的数据方面,而泛型编程关注的是算法。它们之间的共同点是抽象和创建可重用代码,但它们的理念绝然不同
泛型编程旨在编写独立于数据类型的代码。在C++中,完成通用程序的工具是模板。当然,模板使得能够按泛型定义函数或类,而STL通过通用算法更进了一步。理解迭代器是理解STL 的关键所在。模板使得算法独立于存储的数据类型,而迭代器使算法独立于使用的容器类型。因此,它们都是STL通用方法的重要组成部分。
- 在double数组中搜索特定值的函数,可以这样编写该函数:【为了解为何需要迭代器,来看如何为两种不同数据表示实现 find函数】
double * find_ar (double * ar, int n,const double & val){ for (int i = o ; i < n; i++) if (ar [i] == val) return &ar[i] ; return 0; // or, in C++11, return nullptr; } - 假设有一个指向链表第一个节点的指针,每个节点的p_next 指针都指向下一个节点,链表最后一个节点的p_next 指针被设置为0,则可以这样编写
find_ll()函数:
从实现细节上看,这两个find函数的算法是不同的:Node* find_ll (Node * head,const double & val){ Node * start; for (start = head; start!= 0; start = start->p_next) if (start->item == val) return start; return 0; }
一个使用数组索引来遍历元素,另一个则将start重置为start->p_next。
但从广义上说,这两种算法是相同的:
将值依次与容器中的每个值进行比较,直到找到匹配的为止。
泛型编程旨在使用同一个find 函数来处理数组、链表或任何其他容器类型。即函数不仅独立于容器中存储的数据类型,而且独立于容器本身的数据结构。
模板提供了存储在容器中的数据类型的通用表示,因此还需要遍历容器中的值的通用表示,迭代器正是这样的通用表示。 - 使用迭代器修改的
find_ar()typedef double * iterator; iterator find_ar(iterator begin, iterator end,const double & val){ iterator ar; for (ar = begin; ar != end;ar++) if (*ar == val) return ar; return end; // indicates val not found } - 对于
find_ll()函数,可以定义一个迭代器类,其中定义了运算符*和++:
为区分struct Node{ double item; Node * p_next; }; class iterator{ Node * pt; public: iterator() : pt(0){} iterator (Node * pn) : pt (pn){ } double operator* () { return pt->item; }iterator& operator++(){ // for ++it pt = pt->p_next; return *this; } iterator operator++(int){ // for it++ iterator tmp = *this; pt = pt->p_next; return tmp; } // ... operator==( ), operator!=(), etc. };++运算符的前缀版本和后缀版本,C++将operator++作为前缀版本,将operator++ (int)作为后缀版本;其中的参数永远也不会被用到,所以不必指定其名称。
这里重点不是如何定义iterator类,而是有了这样的类后,第二个find函数就可以这样编写:
STL遵循上面介绍的方法。首先,每个容器类(vector、list、deque等)定义了相应的迭代器类型。对于其中的某个类,迭代器可能是指针;而对于另一个类,则可能是对象。iterator find_ll(iterator head,const double & val){ iterator start; for (start = head; start!= 0; ++start) if (*start == val) return start; return 0 ; }
不管实现方式如何,迭代器都将提供所需的操作,如*和++(有些类需要的操作可能比其他类多)。其次,每个容器类都有一个超尾标记,当迭代器递增到超越容器的最后一个值后,这个值将被赋给迭代器。
每个容器类都有begin()和end()方法,它们分别返回一个指向容器的第一个元素和超尾位置的迭代器。每个容器类都使用++操作,让迭代器从指向第一个元素逐步指向超尾位置,从而遍历容器中的每一个元素。使用容器类时,无需知道其迭代器是如何实现的,也无需知道超尾是如何实现的,而只需知道它有迭代器,其begin()返回一个指向第一个元素的迭代器,end()返回一个指向超尾位置的迭代器即可
- 在double数组中搜索特定值的函数,可以这样编写该函数:【为了解为何需要迭代器,来看如何为两种不同数据表示实现 find函数】
-
迭代器
每个容器类都定义了一个合适的迭代器,该迭代器的类型是一个名为 iterator 的 typedef,其作用域为整个类。
例如,要为vector的double类型规范声明一个迭代器,可以这样做:
vector<double> ::iterator pd; // pd an iterator
假设scores是一个vector对象:
vector<double> scores;
则可以使用迭代器pd执行这样的操作:
pd = scores.begin(); // have pd point to the first elemen
*pd = 22.3; // dereference pd and assign value to first element
++pd; // make pd point to the next element
迭代器的行为就像指针。顺便说一句,还有一个C++11自动类型推断很有用的地方。例如,可以不这样做:
vector<double>: :iterator pd = scores.begin();
而这样做:
auto pd = scores.begin();//C++11 automatic type deduction如果it1和it2是迭代器,则STL文档使用
[pl, p2)来表示从p1到p2(不包括p2)的区间。
因此,区间[begin(), end()]将包括集合的所有内容,而区间[pl, pl)为空。
[)表示法并不是C++的组成部分,因此不能在代码中使用,而只能出现在文档中。 -
基于范围的for循环(C++11)
double prices [5] ={4.99,10.99,6.87,7.99,8.49}; for (double x : prices) cout << x<< endl;在这种for 循环中,括号内的代码声明一个类型与容器存储的内容相同的变量,然后指出了容器的名称。接下来,循环体使用指定的变量依次访问容器的每个元素。
for_each(books.begin (), books.end () , ShowReview);
可将其替换为下述基于范围的for循环:
for (auto x : books) ShowReview (x);
根据book 的类型(vector<Review>),编译器将推断出x的类型为Review,而循环将依次将books 中的每个Review对象传递给ShowReview()。
不同于for_each(),基于范围的 for循环可修改容器的内容,诀窍是指定一个引用参数。例如,假设有如下函数:
void InflateReview(Review &r){r.rating++;}
可使用如下循环对books的每个元素执行该函数:
for (auto & x : books) InflateReview(x);迭代器是广义指针,而指针满足所有的迭代器要求。迭代器是STL算法的接口,而指针是迭代器,因此 STL 算法可以使用指针来对基于指针的非STL 容器进行操作。
除了ostream_iterator和 istream_iterator之外,头文件iterator还提供了其他一些专用的预定义迭代器类型。它们是reverse_iterator、back_insert_iterator、front _insert_iterator和 insert_iterator
-
函数对象( functor )
很多STL 算法都使用函数对象——也叫函数符(functor)。函数符是可以以函数方式与()结合使用的任意对象。这包括函数名、指向函数的指针和重载了()运算符的类对象
【即定义了函数operator()()的类】class Linear{ private: double slope; double y0; public: Linear (double sl_ = 1,double y_ = 0) : slope(sl_),yo(y_) { } double operator() (double x){return y0 + slope * x; } };这样,重载的
()运算符将使得能够像函数那样使用Linear对象:Linear f1; Linear f2(2.5,10.0); double y1 = f1(12.5); // right-hand side is f1.operator()(12.5) 0+1*12.5 double y2 = f2(0.4); // 10.0+2.5*0.4函数符概念:
生成器(generator)是不用参数就可以调用的函数符。
一元函数(unary function)是用一个参数可以调用的函数符。
二元函数(binary function)是用两个参数可以调用的函数符。
返回bool值的一元函数是谓词(predicate);
返回bool值的二元函数是二元谓词( binary predicate)。
预定义的函数符:
例如,如果m8是另一个vector<double>对象,mean(double,double)返回两个值的平均值,则下面的的代码将输出来自 gr8和m8的值的平均值:
transform(gr8.begin(), gr8.end (), m8. begin (), out,mean);
现在假设要将两个数组相加。不能将+作为参数,因为对于类型double来说,+是内置的运算符,而不是函数。可以定义一个将两个数相加的函数,然后使用它:
double add (double x,double y){ return x +y; }...
transform(gr8.begin(), gr8.end (), m8. begin (), out,add);然而,这样必须为每种类型单独定义一个函数。更好的办法是定义一个模板(除非STL已经有一个模板了,这样就不必定义)。
头文件functional(以前为function.h)定义了多个模板类函数对象,其中包括plus<>()。可以用plus<>类来完成常规的相加运算:#include <functional> plus<double> add; // create a plus<double> object double y = add(2.2,3.4); // using plus<double> :: operator()() // 它使得将函数对象作为参数很方便: transform(gr8. begin() , gr8.end(), m8.begin(), out,plus<double>());这里,代码没有创建命名的对象,而是用
plus<double>构造函数构造了一个函数符,以完成相加运算【括号表示调用默认的构造函数,传递给transform()的是构造出来的函数对象】。
对于所有内置的算术运算符、关系运算符和逻辑运算符,STL 都提供了等价的函数符。 -
自适应函数符和函数适配器:
STL有5个相关的概念:- 自适应生成器(adaptablegenerator)
- 自适应一元函数(adaptable unary function)
- 自适应二元函数(adaptable binary function)
- 自适应谓词(adaptable predicate)
- 自适应二元谓词( adaptable binary predicate)。
使函数符成为自适应的原因是:它携带了标识参数类型和返回类型的 typedef成员。这些成员分别是result_type、first_argument_type和 second_argument_type。
例如,plus<int>对象的返回类型被标识为plus<int>::result_type,这是int的typedef。函数符自适应性的意义在于:函数适配器对象可以使用函数对象,并认为存在这些typedef 成员。
假设要将矢量gr8的每个元素都增加2.5倍,则需要使用接受一个一元函数参数的transform()版本,就像前面的例子那样:
transform(gr8.begin(), gr8.end(), out,sqrt);
而multiplie()函数符可以执行乘法运行,但它是二元函数。因此需要一个函数适配器,将接受两个参数的函数符转换为接受1个参数的函数符。
STL提供了函数bind1st(),以简化 binder1st 类的使用。可以问其提供用于构建binder1st对象的函数名称和值,它将返回一个这种类型的对象。要将二元函数multiplies()转换为将参数乘以2.5的一元函数,则可以这样做:
bind1st (multiplies<double>(),2.5)
因此,将gr8中的每个元素与2.5相乘:
transform (gr8.begin() , gr8.end(), out, bind1st ( multiplies<double> (),2.5));
binder2nd类与此类似,只是将常数赋给第二个参数,而不是第一个参数。它有一个名为 bind2nd的助手函数,该函数的工作方式类似于bind1st。 -
STL算法
STL包含很多处理容器的非成员函数:sort()、copy()、find()、random_shuffle()、set_union()、set_intersection()、set_difference()和 transform()。
它们的总体设计是相同的,都使用迭代器来标识要处理的数据区间和结果的放置位置。有些函数还接受一个函数对象参数,并使用它来处理数据。
对于算法函数设计,有两个主要的通用部分:- 它们都使用模板来提供泛型;
- 它们都使用迭代器来提供访问容器中数据的通用表示。
STL将算法库分成4组:
- 非修改式序列操作; (以前为algol.h)
对区间中的每个元素进行操作,不修改容器内容【find、for_each】 - 修改式序列操作;
【transform、random_shuffle、copy】 - 排序和相关操作;
- 通用数字运算。(以前也位于algol.h中)
-
模板initializer_list【C++11新增】
可使用初始化列表语法将STL容器初始化为一系列值:
std::vector<double> payments {45.99,39.23,19.95,89.01};
因为容器类现在包含将initializer_list作为参数的构造函数。例如,vector包含一个将initializer_list作为参数的构造函数,因此上述声明与下面的代码等价:
std::vector<double> payments ( {45.99,39.23,19.95,89.01});
除非类要用于处理长度不同的列表,否则让它提供接受initializer_list作为参数的构造函数没有意义。
例如,对于存储固定数目值的类,您不想提供接受initializer_list作为参数的构造函数。在下面的声明中,类包含三个数据成员,因此没有提供initializer_list作为参数的构造函数:
class Position{
private:
int x;int y;int z;
public:
Position (int xx = 0, int yy= 0, int zz =0 ) : x(xx), y(yy), z(zz〉{}
// no initializer_list constructor
};
// 这样,使用语法身时将调用构造函数Position(int, int, int):
Position A = {20,-3 }; // uses Position (20,-3,0)
使用initializer_list:
要在代码中使用initializer_list对象,必须包含头文件initializer_list。这个模板类包含成员函数begin()和end(),还包含成员函数size()。此外要求编译器支持C++11新增的 initializer_list。
#include <iostream>
#include <initializer_list>
double sum(std::initializer_list<double> il); // 可按值传递initializer_list对象,也可按引用传递,如 sum()和 average()所示。
//这种对象本身很小,通常是两个指针(一个指向开头,一个指向末尾的下一个元素),也可能是一个指针和一个表示元素数的整数,因此采用的传递方式不会带来重大的性能影响。STL按值传递它们。
double average(const std::initializer_list<double>& ril);
int eighth_main()
{
using std::cout;
cout << "List 1: sum = " << sum({ 2,3,4 }) << " ,ave = " << average({ 2,3,4 }) << '\n ';
std::initializer_list<double> dl = {1.1, 2.2, 3.3, 4.4, 5.5};
cout << "List 2 : sum = " << sum(dl) << ",ave = " << average(dl) << '\n';
dl = {16.0, 25.0, 32.0, 47.0, 60.0};
cout << "List 3: sum = " << sum(dl) << ",ave = " << average(dl) << '\n';
return 0;
}
double sum(std:: initializer_list<double> il) {
double tot = 0;
for (auto p = il.begin(); p != il.end(); p++)
tot += *p ;
return tot;
}
double average(const std:: initializer_list<double>& ril) {
double tot = 0; int n = ril.size(); double ave = 0.0; if (n > 0)
{
for (auto p = ril.begin(); p != ril.end(); p++)
tot += *p;
ave = tot / n;
}
return ave;
}
9 I/O与C++11新特性
输入、输出与文件
------------------------------------------------------------
C++角度的输入和输出
C++I/O的解决方案是在头文件iostream和fstream中定义的一组类。
C++程序把输入和输出看作字节流。管理输入包含两步:
- 将流与输入去向的程序关联起来。
- 将流与文件连接起来。
------------------------------------------------------------
iostream类系列
iostream文件中包含一些专门设计用来实现、管理流与缓冲区的类。
- streambuf类为缓冲区提供了内存,并提供了用于填充缓冲区、访问缓冲区内容、刷新缓冲区和管理缓冲区内存的类方法;
- ios_base类表示流的一般特征,如是否可读取、是二进制流还是文本流等;
- ios 类基于ios_base,其中包括了一个指向streambuf对象的指针成员;
- ostream类是从ios类派生而来的,提供了输出方法;
- istream类也是从 ios类派生而来的,提供了输入方法;
- cin对象对应于标准输入流【默认情况被关联到标准输入设备(通常为键盘)】。wcin对象与此类似,处理 wchar_t类型。
- cout对象【被关联到标准输出设备(通常为显示器)】。wcout 处理的是wchar_t类型。
- cerr 对象与标准错误流相对应,可用于显示错误消息【关联到标准输出设备(通常为显示器)】。这个流没有被缓冲,这意味着信息将被直接发送给屏幕,而不会等到缓冲区填满或新的换行符。wcerr 处理的是wchar_t类型
- clog对象也对应着标准错误流【默认情况被关联到标准输出设备(通常为显示器)】。这个流被缓冲。wclog 对象处理的是wchar_t类型。
- 对象代表流——当iostream文件为程序声明一个cout对象时,该对象将包含存储了与输出有关的信息的数据成员
- iostream类是基于istream和 ostream类的,因此继承了输入方法和输出方法。
------------------------------------------------------------
重定向
标准输入和输出流通常连接着键盘和屏幕。但很多操作系统(包括UNIX、Linux和 Windows)都支持重定向,这个工具使得能够改变标准输入和标准输出。
例如,假设有一个名为counter.exe 的、可执行的Windows命令提示符C++程序,它能够计算输入中的字符数,开报告结果。该程序的运行情况如下:
C>counter
Hello
and goodbye!
Control-z << simulated end-of-file
Input contained 19 characters.
C>
其中的输入来自键盘,输出的被显示到屏幕上。
通过输入重定向(<)和输出重定向(>),可以使用上述程序计算文件 oklahoma中的字符数,并将结果放到cow_cnt 文件中:
cow_cnt file:
C>counter <oklahoma >cow_cnt
C>
------------------------------------------------------------
ostream类方法
重载的<<运算符(<<默认和C中一样时左移运算符,重载后叫做插入运算符)
刷新缓冲区:flush(cout) 或者 cout<<flush
------------------------------------------------------------
格式化输出
------------------------------------------------------------
istream类方法
使用cin进行输入:cin >> value_holder;
其中,value_holder为存储输入的内存单元,它可以是变量、引用、被解除引用的指针,也可以是类或结构的成员。
cin解释输入的方式取决于value_holder 的数据类型。
istream类(在 iostream头文件中定义)重载了抽取运算符>>,使之能够识别: [基本数据类型] &
get(char&)和get(void)提供不跳过空白的单字符输入功能
get(char*,int,char)和getline(char*,int,char)默认情况下读取整行输入
peek()函数返回输入中的下一个字符,但不抽取输入流中的字符
------------------------------------------------------------
流状态。
不适当的输入【cin或cout对象包含一个描述流状态(stream state)的数据成员(继承自ios_base)】
流状态(被定义为iostate类型,而 iostate是一种bitmask类型)由3个ios_base元素组成: eofbit、badbit或failbit,其中每个元素都是一位,可以是1(设置)或0(清除)。
当cin操作到达文件末尾时,它将设置eofbit;当cin 操作未能读取到预期的字符时(像前一个例子那样),它将设置failbit。
I/O失败(如试图读取不可访问的文件或试图写入写保护的磁盘),也可能将failbit 设置为1。
在一些无法诊断的失败破坏流时,badbit元素将被设置(实现没有必要就哪些情况下设置failbit,哪些情况下设置badbit达成一致)。当全部3个状态位都设置为0时,说明一切顺利。程序可以检查流状态
设置状态:clear()hesetstate()
I/O与异常
exceptions()的默认设置为goodbit【没有引发异常】,但重载的exceptions(iostate)函数能控制其行为:
cin.exceptions(badbit); // setting badbit causes exception to be thrown
cin.exceptions(badbit | eofbit); // 指定多位
------------------------------------------------------------
文件I/O
使用ifstream类从文件输入(get()或者>>运算符)。使用ofstream类输出到文件(write()或者<<插入运算符)。
ofstream:
- 创建一个ofstream 对象来管理输出流;
- 将该对象与特定的文件关联起来;
- 以使用cout的方式使用该对象,唯一的区别是输出将进入文件,而不是屏幕。
ifstream:
- 创建一个ifstream对象来管理输入流;
- 将该对象与特定的文件关联起来;
- 以使用cin的方式使用该对象。
使用fstream类进行文件输入和输出(创建新文件、取代旧文件、文件移动,同步文件I/O)
------------------------------------------------------------
命令行处理
int main(int argc, char* agrv[])
argc为命令行中的参数个数,其中包括命令名本身。argv变量为一个指针,它指向一个指向char的指针
可以将argv看作一个指针数组,其中的指针指向命令行参数,argv[0]是一个指针,指向存储第一个命令行参数的字符串的第一个字符,依此类推。也就是说,argv[0]是命令行中的第一个字符
------------------------------------------------------------
二进制文件
一种在文件中移动的方式。fstream类为此继承了两个方法: seekg()和 seekp(),前者将输入指针移到指定的文件位置,后者将输出指针移到指定的文件位置
(实际上,由于fstream类使用缓冲区来存储中间数据,因此指针指向的是缓冲区中的位置,而不是实际的文件)。
也可以将seekg()用于ifstream对象,将seekp()用于oftream对象。下面是seekg()的原型:
basic_istream<charT,traits>& seekg(off_type,ios_base::seekdir);
basic_istream<charT , traits>& seekg (pos_type);
它们都是模板。对于char具体化,上面两个原型等同于下面的代码:
istream & seekg (streamoff, ios_base:: seekdir);
istream & seekg (streampos);
开发应用程序时,经常需要使用临时文件,这种文件的存在是短暂的,必须受程序控制。
在C++中如何使用临时文件呢? 创建临时文件、复制另一个文件的内容并删除文件其实都很简单。
首先,需要为临时文件制定一个命名方案,但如何确保每个文件都被指定了独一无二的文件名呢? cstdio 中声明的tmpnam()标准函数:
char* tmpnam(char* pszName );
tmpnam()函数创建一个临时文件名,将它放在pszName 指向的C-风格字符串中。
常量L_tmpnam和TMP_MAX(二者都是在cstdio 中定义的)限制了文件名包含的字符数以及在确保当前目录中不生成重复文件名的情况下tmpnam()可被调用的最多次数。
更具体地说,使用tmpnam()可以生成TMP_NAM个不同的文件名,其中每个文件名包含的字符不超过L_tmpnam个。
------------------------------------------------------------
内核格式化
iostream族(family)支持程序与终端之间的I/O,而fstream族使用相同的接口提供程序和文件之间的I/O。
C++库还提供了sstream族,它们使用相同的接口提供程序和string对象之间的I/O。也就是说,可以使用于cout的ostream方法将格式化信息写入到string对象中,并使用istream方法(如 getline())来读取string对象中的信息。
读取string对象中的格式化信息或将格式化信息写入string对象中被称为内核格式化(incore formatting)。
下面简要地介绍一下这些工具(string 的 sstream族支持取代了char数组的 strstream.h族支持)。
头文件 sstream定义了一个从ostream类派生而来的 ostringstream类(还有一个基于 wostream的wostringstream类,这个类用于宽字符集)。
如果创建了一个ostringstream对象,则可以将信息写入其中,它将存储这些信息。可以将可用于cout的方法用于ostringstream对象。
ostringstream outstr;
double price = 380.0 ;
char * ps = " for a copy of the ISO/EIC C++ standard ! " ;
outstr.precision (2);
outstr << fixed;
outstr << "Pay only CHF " << price << ps << endl;
格式化文本进入缓冲区,在需要的情况下,该对象将使用动态内存分配来增大缓冲区。ostringstream类有一个名为str()的成员函数,该函数返回一个被初始化为缓冲区内容的字符串对象:
string mesg = outstr.str(); // returns string with formatted information
使用str()方法可以“冻结”该对象,这样便不能将信息写入该对象中。
探讨C++11的新标准
- 移动语义和右值引用。Lambda表达式。包装器模板 function。可变参数模板。
#include <vector>
#include <string>
#include <iostream>
#include <algorithm>
#include <cmath>
class Stump {
private:
int roots;
int weights;
public:
Stump(int r, double w):roots(r), weights(w){}
};
void func_intialize() {
int x = { 5 };
double y{ 2.75 };
short quar[2]{ 2,3 };
int* ar = new int[3]{ 1,2,3 }; // C++11
Stump s1(3, 15.8); // old style
Stump s2{ 5, 12.3 }; // C++11
Stump s3 = { 4, 4.3 }; // C++11
//然而,如果类有将模板std::initializer_list 作为参数的构造函数,则只有该构造函数可以使用列表初始化形式。
/* 缩窄【初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量】*/
char cl = 1.57e27; // double - to - char, undefined behavior
char c2 = 459585821; // int-to-char, undefined behavior
// 然而,如果使用初始化列表语法,编译器将禁止进行这样的类型转换,即将值存储到比它“窄”的变量中:
char c1{ 1.57e27 }; // double - to - char, compile - time error
char c2 = { 459585821 };// int-to-char,out of range,compile-time error
// 但允许转换为更宽的类型。另外,只要值在较窄类型的取值范围内,将其转换为较窄的类型也是允许的:
char c1{ 66 }; // int - to - char, in range, allowed
double c2 = { 66 }; // int - to - double, allowed
/* initializer_list
C++11提供了模板类initializer_list,可将其用作构造函数的参数,
如果类有接受 initializer_list 作为参数的构造函数,则初始化列表语法就只能用于该构造函数。列表中的元素必须是同一种类型或可转换为同一种类型。
STL 容器提供了将initializer_list作为参数的构造函数:
#include <initializer_list> // 头文件initializer_list 提供了对模板类initializer_list 的支持
double sum(std::initializer_list<double> il);
int main(){
double total = sum( {2.5,3.1,4} ); // 4 converted to 4.0
}
double sum(std::initializer_list<double> il){
double tot = 0;
for (auto p = il.begin(); p !=il.end(); p++) tot += *p; // 这个类包含成员函数 begin()和end(), $这里使用auto简化了模板声明$
return tot;
}
*/
std::vector<int> a1(10); // uninitialized vector with 10 elements
std::vector<int> a2{ 10 }; // initializer-list, a2 has 1 element set to 10
std::vector<int> a3{ 4,6,1 }; // 3 elements set to 4,6,1
}
void func_declaration() {
/* auto C和早期的C++中,关键字auto是一个存储类型说明符,C++11将其用于实现自动类型推断。
这要求进行显式初始化,让编译器能够将变量的类型设置为初始值的类型
*/
auto maton = 112; // maton is type int
auto pt = &maton; // pt is type int *
double fm(double, int);
auto pf = fm; // pf is type double (*)(double, int)
/* decltype 将变量的类型声明为表达式指定的类型
decltype(x) y; // 让y的类型与x相同,其中x是表达式
*/
double x;
int n;
decltype(x * n) q; // q same type as x* n, i.e., double
decltype(&x) pd; // pd same as &x, i.e., double*
/*这在定义模板时特别有用,因为只有等到模板被实例化时才能确定类型:
template<typename T, typename U)
void ef(T t, U u)
{
decltype(T * U) tu; // tu将为表达式T*U的类型【假设T为char、U为short。则tu将为int(整型算术运算自动转换)】
...
}
*/
/* 返回类型后置【C++11新增:在函数名和参数列表后指定返回类型】 */
double f1(double, int); // traditional syntax(句法)
auto f2(double, int) -> double; // new syntax, return type is double
// 利用以上这种特性能结合decltype指定模板函数的返回类型
// template<typename T, typename U>
// auto eff(T t, U u) -> decltype(T*U){ ... } // 在编译器遇到eff 的参数列表前,T和U还不在作用域内,因此必须在参数列表后使用decltype。
/* 模板别名 using =
对于冗长或复杂的标识符,如果能够创建其别名将很方便。
*/
typedef std::vector<std::string>::iterator itType; // traditional C++
using itType = std::vector<std::string>::iterator; // C++11
// 差别在于,新语法也可用于模板部分具体化,但typedef不能
//template<typename T>
//using arr12 = std::array<T, 12>; // template for multiple aliases
/* 空指针 nullptr——不会指向有效数据的指针
之前使用0代表空指针可能出现二义性,因此C++11为了更安全引入nullptr(nullptr==0 返回true)
*/
/* 作用域内枚举
传统的C++枚举提供了一种创建名称常量的方式,但其类型检查相当低级。
另外,枚举名的作用域为枚举定义所属的作用域,这意味着如果在同一个作用域内定义两个枚举,它们的枚举成员不能同名。
最后,枚举可能不是可完全移植的,因为不同的实现可能选择不同的底层类型。
为解决这些问题,C++11新增了一种枚举。这种枚举使用class或struct定义:
*/
enum old1 { yes, no, maybe }; // traditional form
enum class New1 { never, sometimes, often, always }; // new form
enum struct New2 { never, lever, sever }; // new form
// 使用New1::never 和 New2::never
}
void func_class() {
/* 显式转换运算符,使用explict关键字【禁止单构造函数导致的自动转换】
因为C++支持对象自动转换,这在一些场景下会出现问题
*/
/* 类成员初始化
class Session{
int mem1 = 10; // in-class initialization
double mem2 {1966.54 } ; // in-class initialization
short mem3;
public:
session() {} //#1
session(short s) : mem3(s){} // #2
Session(int n, double d,short s) : mem1(n),mem2(d),mem3(s){}// #3
};
可使用等号或大括号版本的初始化,但不能使用圆括号版本的初始化。其结果与给前两个构造函数提供成员初始化列表,并指定mem1和mem2的值相同:
session() : mem1(10), mem2 (1966.54){}
session(short s) : mem1(10),mem2 (1966.54), mem3(s){}
如果构造函数在成员初始化列表中提供了相应的值,这些默认值将被覆盖,因此第三个构造函数覆盖了类内成员初始化。
*/
/* STL方面
对于内置数组以及包含方法 begin()和end()的类(如 std::string)和STL容器,基于范围的for循环可简化为它们编写循环的工作。
*/
double prices[5] = {1.2, 2.3, 3.5, 23.2, 4.5};
for(double x : prices) std::cout<< x << std::endl;
/* 新的STL方法
C++11新增了STL方法cbegin()和cend()【同begin()和end(),返回一个迭代器,但是将元素视为const】
对valarray做了升级(支持begin()和end()方法)
C++98新增了关键字export,能够将模板定义放在接口文件和实现文件中,其中前者包含原型和模板声明,而后者包含模板函数和方法的定义。
实践证明这不现实,因此C++11终止了这种用法,但仍保留了关键字export,供以后使用。
*/
}
void func_left_right_value() {
using namespace std;
/* 左值引用与右值引用 */
int a = 10;
int& b = a; // 定义一个左值引用变量
b = 20; // 通过左值引用修改引用内存的值
// 左值引用在汇编层面其实和普通的指针是一样的;
// 定义引用变量必须初始化,因为引用其实就是一个别名,需要告诉编译器定义的是谁的引用。
//int& var = 10; // 编译错误,因为10无法进行取地址操作,无法对一个立即数取地址,因为立即数并没有在内存中存储,而是存储在寄存器中,可以通过下述方法解决:
const int& var = 10; // 使用常引用来引用常量数字10,因为此刻内存上产生了临时变量保存了10,这个临时变量是可以进行取地址操作的,因此var引用的其实是这个临时变量
// 上面代码等价于下面的操作:
const int temp = 10;
const int& var2 = temp;
/* C++对于左值和右值没有标准定义,但是有一个被广泛认同的说法:
可以取地址的,有名字的,非临时的就是左值;
不能取地址的,没有名字的,临时的就是右值;
可见立即数,函数返回的值等都是右值;而非匿名对象(包括变量),函数返回的引用,const对象等都是左值。
*/
//从本质上理解,创建和销毁由编译器幕后控制,程序员只能确保在本行代码有效的,就是右值(包括立即数);而用户创建的,通过作用域规则可知其生存期的,就是左值(包括函数返回的局部变量的引用以及const对象)。
// 类型&& 引用名 = 右值表达式; // C++ 11新增的特性
// 右值引用用来绑定到右值,绑定到右值以后本来会被销毁的右值的生存期会延长至与绑定到它的右值引用的生存期。
int&& var = 10; // 在汇编层面右值引用做的事情和常引用是相同的,即产生临时量来存储常量。但是,唯一 一点的区别是,右值引用可以进行读写操作,而常引用只能进行读操作。
// 右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。
}
移动语义
// 假设有如下代码
vector<string> vstr;
// build up a vector of 20,000 strings,each of 1000 characters. ..
vector<string> vstr_copy1(vstr); // make vstr_copy1 a copy of vstr
vector 和 string 类都使用动态内存分配,因此它们必须定义使用某种new版本的复制构造函数。
为初始化对象vstr_copy1,复制构造函数vector<string>将使用new给20000个string对象分配内存,而每个string对象又将调用string的复制构造函数,该构造函数使用new为1000个字符分配内存。
接下来,全部20000000个字符都将从vstr控制的内存在复制到vstr_copy1控制的内存中【工作量很大 不妥当】
// 假设还有如下代码
vector<string> allcaps(const vector<string> &vs) {
vector<string> temp;
// code that stores an all - uppercase version of vs in temp
return temp;
}
假设以下面这种方式使用它:
vector<string> vstr;
// build up a vector of 20,000 strings,each of 1000 characters
vector<string> vstr_copy1(vstr); //#1
vector<string> vstr_copy2(allcaps(vstr)); //#2
从表面上看,语句#1和#2类似,它们都使用一个现有的对象初始化一个
vector<string>对象。
如果深入探索这些代码,将发现allcaps()创建了对象temp,该对象管理着20000000个字符;
vector和 string 的复制构造函数创建这20000000个字符的副本,然后程序删除allcaps()返回的临时对象(迟钝的编译器甚至可能将temp复制给一个临时返回对象,删除 temp,再删除临时返回对象)。
这里的要点是,做了大量的无用功。考虑到临时对象被删除了,如果编译器将对数据的所有权直接转让给vstr_copy2,不是更好吗?
也就是说,不将20000000个字符复制到新地方,再删除原来的字符,而将字符留在原来的地方,并将vstr_copy2与之相关联。
这类似于在计算机中移动文件的情形:实际文件还留在原来的地方,而只修改记录。这种方法被称为移动语义(move semantics)。【移动语义实际上避免了移动原始数据,而只是修改了记录】
要实现移动语义,需要采取某种方式,让编译器知道什么时候需要复制,什么时候不需要。这就是右值引用发挥作用的地方。
可定义两个构造函数。其中一个是常规复制构造函数,它使用const 左值引用作为参数,这个引用关联到左值实参,如语句#1中的 vstr;
另一个是移动构造函数,它使用右值引用作为参数,该引用关联到右值实参,如语句#2中 allcaps(vstr)的返回值。
复制构造函数可执行深复制,而移动构造函数只调整记录。在将所有权转移给新对象的过程中,移动构造函数可能修改其实参,这意味着右值引用参数不应是const。
应用:复制构造函数、移动构造函数
class Useless{
private:
int n; // number of elements
char * pt; // pointer to data
static int ct; // number of objects
void ShowObject() const;
public:
Useless();
explicit Useless(int k);
Useless(int k, char ch);
Useless(const Useless & f); // regular copy constructor
Useless(Useless && f); // move constructor
~Useless();
...
};
首先来看复制构造函数(删除了输出语句):
Useless::Useless(const Useless & f): n(f.n){
++ct;
pc = new char[n];
for (int i = 0; i < n; i++) pc[i] = f.pc [i];
}
它执行深复制,是下面的语句将使用的构造函数:
Useless two = one ; // calls copy constructor
引用f将指向左值对象one。
接下来看移动构造函数,这里也删除了输出语句:
Useless::Useless(Useless && f ) : n(f.n){
++ct;
pc = f.pc; // steal address
f.pc = nullptr; // give old object nothing in return
f.n = 0;
}
它让pc 指向现有的数据,以获取这些数据的所有权。此时,pc和f.pc指向相同的数据,调用析构函数时这将带来麻烦,因为程序不能对同一个地址调用delete []两次。
为避免这种问题,该构造函数随后将原来的指针设置为空指针,因为对空指针执行delete []没有问题。
这种夺取所有权的方式常被称为窃取(pilfering)。
上述代码还将原始对象的元素数设置为零,这并非必不可少的,但让这个示例的输出更一致。
注意,由于修改了f对象,这要求不能在参数声明中使用const。
在下面的语句中,将使用这个构造函数:
Useless four (one + three); // calls move constructor 表达式one+three是右值
*/
}
void new_classFunc() {
/*
在原有4个特殊成员函数(默认构造函数、复制构造函数、复制赋值运算符和析构函数)的基础上,C++11新增了两个:移动构造函数 和 移动赋值运算符。
在没有提供任何参数的情况下,将调用默认构造函数。如果您没有给类定义任何构造函数,编译器将提供一个默认构造函数。这种版本的默认构造函数被称为默认的默认构造函数。
对于使用内置类型的成员,默认的默认构造函数不对其进行初始化;对于属于类对象的成员,则调用其默认构造函数。
另外,如果没有提供复制构造函数,而代码又需要使用它,编译器将提供一个默认的复制构造函数;
如果没有提供移动构造函数,而代码又需要使用它,编译器将提供一个默认的移动构造函数。
假定类名为Someclass,这两个默认的构造函数的原型如下:
Someclass::Someclass(const Someclass &); // defaulted copy constructor
Someclass::Someclass(Someclass &&); // defaulted move constructor
在类似的情况下,编译器将提供默认的复制运算符和默认的移动运算符,它们的原型如下:
Someclass & Someclass::operator(const Someclass &); // defaulted copy assignment
Someclass & Someclass::operator(Someclass &&); // defaulted move assignment
如果没有提供析构函数,编译器将提供一个。
对于前面描述的情况,有一些例外。
如果提供了析构函数、复制构造函数或复制赋值运算符,编译器将不会自动提供移动构造函数和移动赋值运算符;
如果提供了移动构造函数或移动赋值运算符,编译器将不会自动提供复制构造函数和复制赋值运算符。
也可以使用default显式声明这些方法的默认版本(即使提供了)
class Someclass{
public:
Someclass(Someclass &&);
Someclass() = default; // use compiler-generated default constructor
Someclass(const Someclass &) = delete;
Someclass & operator=(const Someclass &) = delete;
...
}
// delete可用于禁止编译器使用特定方法(如要禁止复制对象,可禁用复制构造函数和复制赋值运算符)
*/
/* 管理虚方法:override和final
在C++11中,可使用虚说明符override指出您要覆盖一个虚函数:将其放在参数列表后面。如果声明与基类方法不匹配,编译器将视为错误。
因此,下面的 Bingo::f()版本将生成一条编译错误消息:
virtual void f(char * ch) const override { std ::cout << val() << ch << " ! \n"; }
说明符final解决了另一个问题。您可能想禁止派生类覆盖特定的虚方法,为此可在参数列表后面加上final。例如,下面的代码禁止Action的派生类重新定义函数f():
virtual void f(char ch) const final { std::cout << val() << ch << " \n" ; }
说明符override和 final并非关键字,而是具有特殊含义的标识符。这意味着编译器根据上下文确定它们是否有特殊含义;
在其他上下文中,可将它们用作常规标识符,如变量名或枚举。
*/
}
void lambda_func() {
/* 使用三种方法给STL算法传递信息:函数指针、函数符和lambda */
// 假设要生成一个随机数,并判断其中多少个整数可被3整除,多少个能被13整除
// 方案1:(使用vector<int>存储上数字,使用STL算法generate在其中生成随机数)
std::vector<int> numbers[1000];
std::generate(begin(numbers), end(numbers), std::rand); // 若使用循环 auto it = begin(numbers);
// 方案2:函数符 类方法operator()()
/* class f_mod{
private:
int dv;
public:
f_mod(int d=1) : dv(d){}
bool operator()(int x){ return x % dv == 0; }
};
f_mod obj(3); // f_mod.dv set to 3
bppl is_div_by_3 = obj(7); // same as obj.operator()(7)
*/
// 方案3:使用lambda(使用匿名函数),比如编写”整数可被3整除“的函数:
// [](int x) { return x % 3 == 0; } // 等价于 bool f3(int x){ return x % 3 == 0; }
//差别有两个:使用[]替代了函数名(这就是匿名的由来);没有声明返回类型。返回类型相当于使用decltyp根据返回值推断得到的,这里为bool。
// 如果lambda不包含返回语句,推断出的返回类型将为void。
//就这个示例而言,将以如下方式使用该lambda:
int count3 = std::count_if(begin(numbers), end(numbers), [](int x) {return x % 3 == 0; }); // 也就是说,使用使用整个lambad表达式替换函数指针或函数符构造函数。
//仅当lambad表达式完全由一条返回语句组成时,自动类型推断才管用;否则,需要使用新增的返回类型后置语法:
[](double x)->double {int y = x; return x - y; }; // return type is double
//这个Lambda表达式定义了一个函数对象add,它的参数列表是两个int类型的参数a和b,返回值类型是int。函数体中简单地将两个参数相加并返回。
auto add = [](int a, int b) -> int { return a + b; }; // cout << add(1, 2) << endl;
//Lambda表达式还可以通过捕获列表捕获外部变量。捕获列表可以有三种形式:[]、[&]、[=],分别表示不捕获任何变量、按引用捕获所有外部变量、按值捕获所有外部变量
int x = 1, y = 2;
auto f1 = [x, &y]() mutable { x++; y++; std::cout << x << " " << y << std::endl; };
auto f2 = [=]() { std::cout << x << " " << y << std::endl; };
auto f3 = [&]() { x++; y++; std::cout << x << " " << y << std::endl; };
f1(); //输出2 3
f2(); //输出1 2
f3(); //输出2 3
// 为何要使用Lambda:定义和使用是在同一个地方进行,方便阅览与修改
}
void func_wrapper() {
/*
C++提供了多个包装器(wrapper,也叫适配器[adapter])。这些对象用于给其他编程接口提供更一致或更合适的接口
C++11提供了其它的包装器,包括模板bind、mem_fn和reference_wrapper以及包装器function
bind可替代bind1st和bind2nd,更灵活
mem_fn够将成员函数作为常规函数进行传递;
模板reference_wrapper能够创建行为像引用但可被复制的对象;
包装器function能够以统一的方式处理多种类似于函数的形式
*/
/* 包装器function及模板的低效性
answer = ef(a);
ef是什么呢?它可以是函数名、函数指针、函数对象或有名称的 lambda表达式,
这些都是可调用的类型(callable type)。鉴于可调用的类型如此丰富,这可能导致模板的效率极低
template <typename T, typename F>
T use_f(T v,Ff){
static int count = 0;count++;
cout <<"use_f count = " << count << " ,&count = " << &count << endl;
return f(v); // 模板use_f使用参数f表示调用类型
}
class Fp{
private:
double z_i
public:
Fp (double z = 1.0) : z_(z){}
double operator () (double p){ return z_*p;}
};
class Fq{
private:
double z_;
public:
Fq(double z = 1.0) : z_(z){}
double operator() (double q){ return z_+ q; }
};
double dub(double x){ return 2.0*x; }
double square(double x) { return x*x; }
int main(){
using std::cout; using std::endl ;
double y = 1.21;
cout <<"Function pointer dub : \n";
cout << " " << use_f(y, dub) << endl; // F的类型为double(*)(double)
cout <<"Function pointer square : \n" ;
cout << " " << use_f(y, square) << endl;
cout <<"Function object Fp : \n" ; // F的类型也为double(*)(double) 同样的调用
cout <<" " << use_f(y, Fp(5.0)) << endl;
cout << "Function object Fq :\n" ;
cout << " " << use_f(y, Fq(5.0)) << endl;
cout << "Lambda expression 1 : \n" ;
cout<< " " << use_f(y,[](double u){return u*u; }) << endl;
cout <<"Lambda expression 2 : \n" ;
cout << " " << use_f(y,[](double u) {return u+u/2.0 ; }) << endl;
return 0 ;
}
每次调用中,模板参数T都被设置为类型double,模板参数F接受一个double值并返回一个double值,因此在6次use_of()调用中,理想值实例化模板一次,但是输出表明 实际实例化了6次
包装器function让您能够重写上述程序,使其只使用use f()的一个实例而不是5个。
模板function是在头文件 functional 中声明的,它从调用特征标的角度定义了一个对象,可用于包装调用特征标相同的函数指针、函数对象或lambda表达式。
例如,下面的声明创建一个名为fdci的 function对象,它接受一个char参数和一个int参数,并返回一个 double值:
std::function<double(char, int)> fdci ;
然后,可以将接受一个char参数和一个int参数,并返回一个double值的任何函数指针、函数对象或lambda表达式赋给它。
上面例子改写如下:
int main(){
using std::cout; using std::endl ;
double y = 1.21;
function<double(double)> ef1 = dub;
function<double(double)> ef2 = square;
function<double(double)> ef3 = Fq (10.0);
function<double(double)> ef4 = Fp (10.0);
function<double(double)> ef5 = [](double u) {return u*u; };
function<double(double)> ef6 = [](double u) {return u+u/2.0 ;};
cout <<"Function pointer dub : \n";
cout << " " << use_f(y, ef1) << endl; // F的类型为double(*)(double)
cout <<"Function pointer square : \n" ;
cout << " " << use_f(y, ef2) << endl;
cout <<"Function object Fp : \n" ; // F的类型也为double(*)(double) 同样的调用
cout <<" " << use_f(y, ef3) << endl;
cout << "Function object Fq :\n" ;
cout << " " << use_f(y, ef4) << endl;
cout << "Lambda expression 1 : \n" ;
cout<< " " << use_f(y,ef5) << endl;
cout <<"Lambda expression 2 : \n" ;
cout << " " << use_f(y,ef5) << endl;
return 0 ;
}
也可改写如下:
不用声明6个function<double(double)>对象,而只使用一个临时function<double(double)>对象,将其用作函数use_f()的参数:
typedef function<double(double)> fdd; // simplify the type declaration
cout c< use_f(y, fdd(dub)) << endl; // create and initialize object to dub
cout << use_f(y, fdd (square)) << endl ;
...
*/
}
可变参数模板
可变参数模板((variadic template)让您能够创建这样的模板函数和模板类,即可接受可变数量的参数。
要创建可变参数模板,需要理解几个要点:
- 模板参数包( parameter pack);
- 函数参数包;
- 展开( unpack)参数包;
- 递归。
C++11提供了一个用省略号表示的元运算符(meta-operator),能够声明表示模板参数包的标识符,模板参数包基本上是一个类型列表。
同样,它还能够声明表示函数参数包的标识符,而函数参数包基本上是一个值列表。其语法如下:
templatectypename... Args> // Args is a template parameter pack
void show_list1(Args... args) // args is a function parameter pack
{ ... }
其中,Args是一个模板参数包,而 args是一个函数参数包。
与其他参数名一样,可将这些参数包的名称指定为任何符合C++标识符规则的名称。
Args和T的差别在于,T与一种类型匹配,而 Args 与任意数量(包括零)的类型匹配。
691

被折叠的 条评论
为什么被折叠?



