C++ 基础知识点整理

library
category
tags
C/C++
未整理
attribute
笔记
source
date
Mar 21, 2020
slug
cplusplus-basic-knowledge
status
Published
summary
主要记录 2014 年传智播客 C++ 语言基础视频教程扫地僧老师的课程内容。
1. C++对C的扩展1.1 namespace命名空间1.1.1 C++命名空间基本常识1.1.2 C++命名空间定义及使用语法1.2 「实用性」增加1.3 register关键字增强1.4 变量检测增强1.5 struct类型加强1.6 C++中所有的变量和函数都必须有类型1.7 新增bool类型关键字1.8 三目运算符功能增强1.9 C与C++中的const1.9.1 const基础知识1.9.2 C与C++中const的区别1.9.3 const与#define1.10 C++对C的函数扩展1.10.1 inline内联函数1.10.2 默认参数1.10.3 占位参数1.10.4 默认参数和占位参数结合1.10.5 函数重载1.11 引用1.11.1 引用当返回值1.11.2 常引用2. 类和对象2.1 类和对象的基本概念2.2 对象2.2.1 构造函数和析构函数2.2.2 拷贝构造函数2.2.3 深拷贝和浅拷贝2.2.4 对象初始化列表2.2.5 对象的动态建立和释放2.2.6 静态成员变量和静态成员函数2.2.7 有元2.2.8 运算符重载2.3 继承和派生2.3.1 派生类的访问控制2.3.2 继承中的构造析构调用2.3.3 继承中的同名成员2.3.4 继承中的static关键字2.3.5 多继承2.3.6 虚继承2.4 多态2.4.1 C++中的多态2.4.2 联编2.4.3 vtable虚函数表和vptr虚函数表指针2.5 纯虚函数和抽象类3. 泛型编程与模板3.1 函数模板3.2 类模板4. 异常处理4.1 抛出异常4.2 捕获异常4.3 标准异常4.4 自定义异常5. 文件和流5.1 I/O流概念和流类库结构5.1.1 C++与C的I/O类型安全和可扩展性5.1.2 与iostream类库有关的头文件5.1.3 在iostream头文件中定义的流对象5.1.4 在iostream头文件中重载运算符5.2 标准I/O流5.2.1 cout流对象5.2.2 cerr流对象5.2.3 clog流对象5.2.4 缓冲区的概念5.2.5 标准输入流5.2.6 标准输出流5.3 文件I/O5.3.1 打开文件5.3.2 关闭文件5.3.3 写入文件5.3.4 读取文件References

1. C++对C的扩展

1.1 namespace命名空间

1.1.1 C++命名空间基本常识

所谓namespace,是指标识符的各种可见范围。C++标准程序库中的所有标识符都被定义于一个名为stdnamespace中。
<iostream><iostream.h>格式不一样,前者没有后缀,实际上,在编译器include文件夹里面可以看到,二者是两个文件,打开文件就会发现,里面的代码是不一样的。后缀为.h的头文件C++标准已经明确提出不支持了,早些的实现将标准库功能定义在全局空间里,声明在带.h后缀的头文件里,C++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h。 因此:
  • 当使用<iostream.h>时,相当于在C中调用库函数,使用的是全局命名空间,也就是早期的C++实现
  • 当使用<iostream>的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用cout
由于namespace的概念,使用C++标准程序库的任何标识符时,可以有三种选择:
  1. 直接指定标识符。例如std::ostream而不是ostream。完整语句如下:std::cout << std::hex << 3.4 << std::endl;
  1. 使用using关键字。using std::cout; using std::endl; using std::cin;以上程序可以写成 cout << std::hex << 3.4 << endl;
  1. 最方便的就是使用using namespace std; 例如:using namespace std;。这样命名空间std内定义的所有标识符都有效(曝光),就好像它们被声明为全局变量一样。那么以上语句可以如下写:cout <<hex << 3.4 << endl;
    1. 因为标准库非常的庞大,所以程序员在选择的类的名称或函数名时就很有可能和标准库中的某个名字相同。所以为了避免这种情况所造成的名字冲突,就把标准库中的一切都被放在名字空间std中。但这又会带来了一个新问 题。无数原有的C++代码都依赖于使用了多年的伪标准库中的功能,他们都是在全局空间下的。所以就有了<iostream.h><iostream>等等这样的头文件,一个是为了兼容以前的C++代码,一个是为了支持新的标准。命名空间std封装的是标准程序库的名称,标准程序库为了和以前的头文件区别,一般不加.h

1.1.2 C++命名空间定义及使用语法

在C++中,名称(name)可以是符号常量、变量、宏、函数、结构、枚举、类和对象等等。为了避免,在大规模程序的设计中,以及在程序员使用各种各样的C++库时,这些标识符的命名发生冲突,标准C++引入了关键字namespace,译作命名空间、名字空间、名称空间、名域等,可以更好地控制标识符的作用域。
std是C++标准命名空间,C++标准程序库中的所有标识符都被定义在std中,比如标准库中的类iostreamvector等都定义在该命名空间中,使用时要加上using声明:using namespace std,或using指示,如:std::stringstd::vector<int>
C中的命名空间:
  • 在C语言中只有一个全局作用域
  • C语言中所有的全局标识符共享同一个作用域
  • 标识符之间可能发生冲突
C++中提出了命名空间的概念:
  • 命名空间将全局作用域分成不同的部分
  • 不同命名空间中的标识符可以同名而不会发生冲突
  • 命名空间可以相互嵌套
  • 全局作用域也叫默认命名空间
默认情况下可以直接使用默认命名空间中的所有标识符。
总结:
  • 当使用<iostream>的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用cout。若不引入using namespace std ,需要这样做:std::cout
  • C++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h
  • C++命名空间的定义:namespace name { … }
  • namespce定义可嵌套:using namespace NameSpaceA;

1.2 「实用性」增加

C语言中的变量都必须在作用域开始的位置定义,而在C++中更强调语言的「实用性」,所有的变量都可以在需要使用时再定义。
// Cint i;for(i=0; i<10; i++){    // ...}// C++for(int i=0; i<10; i++){    // ...}

1.3 register关键字增强

register关键字:请求编译器让变量直接放在寄存器里面,运算速度更快。在C语言中register修饰的变量不能取地址,但是在C++里面做了增强:
  • C++中可以取得register变量的地址,C++编译器有自己的优化方式,不使用register也可能做优化
  • C++编译器发现程序中需要取register变量的地址时,register对变量的声明将变得无效。
  • 早期C语言编译器不会对代码进行优化,因此register变量是一个很好的补充
int main22(){    register int a = 0;    printf("&a = %x\n", &a);    return 0;}
register修饰符暗示编译程序相应的变量将被频繁地使用,如果可能的话,应将其保存在CPU的寄存器中,以加快其存储速度,而不是将其储存在内存里面。但register只是建议型的指令,而不是命令型的指令,不一定保证能成功将其保存在CPU的寄存器中。

1.4 变量检测增强

在C语言中,重复定义多个同名的全局变量是合法的。在C++中,不允许定义多个同名的全局变量
C语言中多个同名的全局变量最终会被链接到全局数据区的同一个地址空间上:
int g_var;int g_var = 1;
C++直接拒绝这种二义性的做法。

1.5 struct类型加强

C语言的struct定义了一组变量的集合,C编译器并不认为这是一种新的类型。C++中的struct是一个新类型的定义声明。
struct Student{    char name[100];    int age;};int main(int argc, char *argv[]){    Student s1 = {"wang", 1};    Student s2 = {"wang2", 2};    return 0;}

1.6 C++中所有的变量和函数都必须有类型

C++中所有的变量和函数都必须有类型,C语言中的默认类型在C++中是不合法的。
在C语言中:
  • int f(){}表示返回值为int,接受任意参数的函数
  • int f(void){}表示返回值为int的无参函数
而在C++中:
  • int f(){};int f(void)具有相同的意义,都表示返回值为int无参函数
C++更加强调类型,任意的程序元素都必须显示指明类型。

1.7 新增bool类型关键字

C++在C语言的基本类型系统之上增加了bool,C++中的bool可取的值只有truefalse。理论上bool只占用一个字节,如果多个bool变量定义在一起,可能会各占一个bit,这取决于编译器的实现。
  • true代表真值,编译器内部用1来表示
  • false代表非真值,编译器内部用0来表示
bool类型只有true(非0)和false(0)两个值,C++编译器也会在赋值时将非0值转换为true,0值转换为false

1.8 三目运算符功能增强

在C语言中,三目运算符是表达式,表达式不能做为左值使用,而C++中的三目运算符可直接返回变量本身,因此可以出现在程序的任何地方
(a < b ? 1 : b )= 30;
注意:三目运算符可能返回的值中如果有一个是常量值,则不能作为左值使用
当左值的条件为要有内存空间,C++编译器则帮助程序员取了一个地址。

1.9 C与C++中的const

1.9.1 const基础知识

代表一个常整型数:
const int a;int const b;
指向常整型数的指针(所指向的内存数据不能被修改,但是本身可以修改):
const int *c;
常指针(指针变量不能被修改,但是它所指向内存空间可以被修改):
int * const d;
指向常整型的常指针(指针和它所指向的内存空间,均不能被修改):
const int * const e;
在指针做函数参数时,指定为const可以有效的提高代码可读性,减少bug。
int setTeacher_err( const Teacher *p)
const修饰的形参,不能修改指针所向的内存空间。

1.9.2 C与C++中const的区别

C语言中const变量是只读变量,有自己的存储空间。
C++中当碰见常量声明时,在符号表中放入常量,编译过程中若发现使用常量则直接以符号表中的值替换,若发现对const使用了extern或者&操作符,则给对应的常量分配存储空间(兼容C语言)。
  • const常量为全局,并且需要在其它文件中使用,会分配存储空间
  • 当使用&操作符,取const常量的地址时,会分配存储空间
  • 当类似const int &a = 10;const修饰引用时,也会分配存储空间
注意:C++编译器虽然可能为const常量分配空间,但不会使用其存储空间中的值。

1.9.3 const与#define

C++中的const修饰的,是一个真正的常量,而不是C语言中的只读变量。在const修饰的常量编译期间,就已经确定下来了。
C++中的const常量类似于宏定义:
const int c = 5;
约等于:
#define c 5
但C++中的const常量与宏定义不同,const常量是由编译器处理的,提供类型检查作用域检查,宏定义由预处理器处理,只是单纯的文本替换。

1.10 C++对C的函数扩展

1.10.1 inline内联函数

C++中的const常量可以替代宏常数定义,如:
const int A = 3;//#define A 3
C++中是否有解决方案替代宏代码片段呢?替代宏代码片段就可以避免宏的副作用。
C++编译器可以将一个函数进行内联编译,被C++编译器内联编译的函数叫做内联函数,推荐使用内联函数替代宏代码片段,使用inline关键字声明内联函数:
#include "iostream"using namespace std;// 宏定义#define MYFUNC(a, b) ((a) < (b) ? (a) : (b))// 内联函数inline int myfunc(int a, int b){    return a < b ? a : b;}int main(){    int a = 1;    int b = 3;    int c = myfunc(++a, b);    //int c = MYFUNC(++a, b);    printf("a = %d\n", a);    printf("b = %d\n", b);    printf("c = %d\n", c);    return 0;}
内联函数声明时inline关键字必须和函数定义、函数体的实现结合在一起,否则编译器会直接忽略内联请求。
内联函数在最终生成的代码中是没有定义的,C++编译器直接将函数体插入在函数调用的地方。内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)。需要注意的是,C++编译器不一定准许函数的内联请求!
内联函数是一种特殊的函数,具有普通函数的特征(参数检查,返回类型等),内联函数是对编译器的一种请求,因此编译器可能拒绝这种请求。
  • 内联函数由编译器处理,直接将编译后的函数体插入调用的地方
  • 宏代码片段由预处理器处理,进行简单的文本替换,没有任何编译过程
现代C++编译器能够进行编译优化,因此一些函数即使没有inline声明,也可能被编译器内联编译。另外,一些现代C++编译器提供了扩展语法,能够对函数进行强制内联如:g++中的__attribute__((always_inline))属性。
C++中进行内联编译的限制条件:
  • 不能存在任何形式的循环语句
  • 不能存在过多的条件判断语句
  • 函数体不能过于庞大
  • 不能对函数进行取址操作
  • 函数内联声明必须在调用语句之前
编译器对于内联函数的限制并不是绝对的,内联函数相对于普通函数的优势只是省去了函数调用时压栈、跳转和返回的开销。因此,当函数体的执行开销远大于压栈、跳转和返回所用的开销时,那么内联将无意义

1.10.2 默认参数

C++中可以在函数声明时为参数提供一个默认值,当函数调用时没有指定这个参数的值,编译器会自动用默认值代替:
void myPrint(int x = 3){    return x;}
函数默认参数的规则:
只有参数列表后面部分的参数才可以提供默认参数值,一旦在一个函数调用中开始使用默认参数值,那么这个参数后的所有参数都必须使用默认参数值。

1.10.3 占位参数

占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数。
int func(int a, int b, int ){    return a + b;}

1.10.4 默认参数和占位参数结合

可以将占位参数与默认参数结合起来使用,可以为以后程序的扩展留下线索,也可以兼容C语言程序中可能出现的不规范写法。
int func2(int a, int b, int = 0){    return a + b;}void main(){    // 如果默认参数和占位参数在一起,都能调用起来    func2(1, 2);    func2(1, 2, 3);}

1.10.5 函数重载

所谓重载,就是重新赋予新的含义。函数重载就是对一个已有的函数赋予新的含义,使之实现新功能,因此,一个函数名就可以用来代表不同功能的函数,也就是「一名多用」。
当相同的函数名和不同的参数搭配时函数的含义也不同:
int func(int x){    return x;}int func(int a, int b){    return a + b;}int func(const char* s){    return strlen(s);}int main(){    int c = 0;    c = func(1);    printf("c = %d\n", c);    c = func(1, 2);    printf("c = %d\n", c);    c = func("12345");    printf("c = %d\n", c);    return 0;}
函数重载至少满足下面的一个条件:
  • 参数个数不同
  • 参数类型不同
  • 参数顺序不同
函数重载的注意事项:
  • 重载函数在本质上是相互独立的不同函数(静态链编)
  • 重载函数的函数类型是不同的
  • 函数返回值不能作为函数重载的依据
  • 函数重载是由函数名和参数列表决定的。
函数重载是发生在同一个类中
当函数重载遇上函数默认参数:
// 当函数默认参数遇上函数重载会发生什么int func(int a, int b, int c = 0){    return a * b * c;}int func(int a, int b){    return a + b;}// 1个参数的允许int func(int a){    return a + b;}int main(){    int c = 0;    c = func(1, 2);     // 存在二义性,调用失败,编译不能通过    printf("c = %d\n", c);    return 0;}
函数重载和函数指针结合:
int func(int x) // int(int a){    return x;}int func(int a, int b){    return a + b;}int func(const char* s){    return strlen(s);}typedef int(*PFUNC)(int a); // int(int a)int main(){    int c = 0;    PFUNC p = func;    c = p(1);    printf("c = %d\n", c);    return 0;}
当使用重载函数名对函数指针进行赋值时,根据重载规则挑选与函数指针参数列表一致的候选者,严格匹配候选者的函数类型与函数指针的函数类型。

1.11 引用

引用是C++中新增的概念,引用可以看作一个已定义变量的别名:
Type& name = var;
引用在C++中的内部实现其实是一个常指针
Type* const name = var;
C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同(标准规定引用是否具有存储空间是未指定的,多数编译器的引用都具有存储空间):
8.3.2 ReferencesIt is unspecified whether or not a reference requires storage (3.7).
从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是C++为了实用性而做出的细节隐藏。引用作为其它变量的别名而存在,因此在一些场合可以代替指针,引用相对于指针来说具有更好的可读性和实用性。
间接赋值成立的三个条件:
  1. 定义两个变量 (一个实参一个形参)
  1. 建立关联(实参取地址传给形参)
  1. p形参去间接的修改实参的值
引用在实现上,只不过是把间接赋值成立的三个条件的后两步和二为一,当实参传给形参引用的时候,只不过是C++编译器帮我们程序员手工取了一个实参地址,传给了形参引用(常量指针)。

1.11.1 引用当返回值

当函数返回值为引用时若返回栈变量:
  • 不能成为其它引用的初始值
  • 不能作为左值使用
若返回静态变量或全局变量:
  • 可以成为其他引用的初始值
  • 即可作为右值使用,也可作为左值使用
如果返回引用不是基础类型而是一个类,那么情况变得复杂,可能会涉及到拷贝构造函数和操作符重载。
引用作为返回值,必须遵守以下规则:
  • 不能返回局部变量的引用(局部变量会在函数返回后被销毁,会使程序进入未知状态,部分编译器会报错)
  • 不能返回函数内部new分配的内存的引用
  • 可以返回类成员的引用,但最好是const
#include <iostream>using namespace std;int &test1(){    int i;  // 返回局部变量    i = 10;    return i;}void test2(){    int a = 0;  // 覆盖了引用a指向的位置}int main(){    int &a = test1();   // test1的局部变量在函数结束后被销毁,但引用指向的位置还在,a = 10    test2();    cout << a << endl;  // a = 0    return 0;}
用引用返回一个函数值的最大好处是,在内存中不产生被返回值的副本。
#include <iostream>using namespace std;float temp;                     // 定义全局变量temp// 以返回值的方法返回函数值float fn1(float r){    temp = (float) (r * r * 3.14);    return temp;}// 以引用方式返回函数值float &fn2(float r){    temp = (float) (r * r * 3.14);    return temp;}int main(){    float a = fn1(10.0);    // 第1种情况,系统生成要返回值的副本(即临时变量)//    float &b = fn1(10.0);   // 第2种情况,不能从被调函数中返回一个临时变量或局部变量的引用    float c = fn2(10.0);    // 第3种情况,系统不生成返回值的副本    float &d = fn2(10.0);   // 第4种情况,可以从被调函数中返回一个全局变量的引用,系统不生成返回值的副本    cout << a << endl;    cout << c << endl;    cout << d << endl;    return 0;}

1.11.2 常引用

在C++中可以声明const引用:
const Type& name = var
const引用让变量拥有只读性质。
使用变量初始化常引用:
const int& a = b;
使用字面量初始化常引用:
const int& a = 10;
当使用常量(字面量)对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名。使用字面量对const引用初始化后,将生成一个只读变量。

2. 类和对象

2.1 类和对象的基本概念

#include <iostream>#include <cmath>using namespace std;class Circle{public:    double getArea(double r)    {        return pi * pow(r, 2);    }private:    double pi = 3.1415926;};int main(){    Circle c;    cout << c.getArea(20.) << endl;    return 0;}
面向对象三大特征:抽象继承多态
抽象(封装):是面向对象程序设计最基本的特性。把数据(属性)和函数(操作)合成一个整体,这在计算机世界中是用类与对象实现的。把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
  • 成员变量:C++中用于表示类属性的变量
  • 成员函数:C++中用于表示类行为的函数
类成员的访问控制:
  • public修饰成员变量和成员函数可以在类的内部和类的外部被访问
  • private修饰成员变量和成员函数只能在类的内部被访问(如果不指定修饰符,这默认为private
  • protected修饰成员变量和成员函数既可以在类的内部,也可以在其派生类(子类)中被访问。
structclass关键字区别:
  • 在用struct定义类时,所有成员的默认属性为public
  • 在用class定义类时,所有成员的默认属性为private

2.2 对象

创建一个对象时,常常需要作某些初始化的工作,例如对数据成员赋初值。类的数据成员是不能在声明类时初始化的,为了解决这个问题,C++编译器提供了构造函数(constructor)来处理对象的初始化。构造函数是一种特殊的成员函数,与其他成员函数不同,不需要用户来调用它,而是在建立对象时自动执行。

2.2.1 构造函数和析构函数

C++中的类可以定义与类名相同的特殊成员函数,这种与类名相同的成员函数叫做构造函数
#include <iostream>#include <cmath>using namespace std;class Circle{public:    Circle(double pi)       // 构造函数与类同名    {        this->pi = pi;    }    double getArea(double r)    {        return pi * pow(r, 2);    }private:    double pi = 3.1415926;};int main(){    Circle c = Circle(3.1);    cout << c.getArea(20.) << endl;    return 0;}
  • 构造函数在定义时可以有参数
  • 构造函数没有任何返回类型的声明
构造函数的调用:
  • 自动调用:一般情况下C++编译器会自动调用构造函数
  • 手动调用:在一些情况下则需要手动调用构造函数
C++中的类可以定义一个特殊的成员函数来清理对象,这个特殊的成员函数叫做析构函数
#include <iostream>#include <cmath>using namespace std;class Circle{public:    Circle(double pi)    {        this->pi = pi;    }    double getArea(double r)    {        return pi * pow(r, 2);    }    ~Circle()           // 析构函数语法为:~类名    {        cout << "destructor" << endl;    }private:    double pi = 3.1415926;};int main(){    Circle c = Circle(3.1);    cout << c.getArea(20.) << endl;    return 0;}
  • 析构函数没有参数也没有任何返回类型的声明
  • 析构函数在对象销毁时由C++编译器自动调用
当类中有成员变量是其它类的对象时,首先调用成员变量的构造函数,调用顺序与声明顺序相同,之后调用自身类的构造函数。析构函数的调用顺序与对应的构造函数调用顺序相反。

2.2.2 拷贝构造函数

拷贝构造函数(也有称复制构造函数)是一种特殊的构造函数,它在创建对象时,是使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:
  • 通过使用另一个同类型的对象来初始化新创建的对象。
  • 复制对象把它作为参数传递给函数。
  • 复制对象,并从函数返回这个对象。
如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。拷贝构造函数的最常见形式如下:
A(const A &obj2){    cout<<"我也是构造函数,我是通过另外一个对象obj2,来初始化我自己"<<endl;    a = obj2.a + 10;}
拷贝构造函数的用法:
A a1;A a2 = a1;      // 用对象1 初始化 对象2
A a1;A a2(a1);       // 用对象1 初始化 对象2
对象初始化操作和=等号操作 是两个不同的概念:
A a1(3);A a2;a2 = a1;
img

2.2.3 深拷贝和浅拷贝

  • 默认拷贝构造函数可以完成对象的数据成员值简单的复制
  • 对象的数据资源是由指针指向的堆时,C++编译器生成的默认拷贝构造函数仅作指针值复制
img
img
解决默认拷贝构造函数浅拷贝问题:
  • 显示提供拷贝构造函数
Name(const Name &obj){    // 用obj来初始化自己    pName = (char *)malloc(obj.size + 1);    strcpy(pName, obj.pName);    size = obj.size;}
  • 显示重载=号操作符,不使用C++编译器提供的浅拷贝
void operator=(Name &obj3){    if (pName != NULL)    {        free(pName);        pName = NULL;        size = 0;    }    // 当obj3用=等号来赋值给自己    pName = (char *)malloc(obj3.size + 1);    strcpy(pName, obj3.pName);    size = obj3.size;}

2.2.4 对象初始化列表

C++中提供初始化列表对成员变量进行初始化,语法规则:
Constructor::Contructor() : m1(v1), m2(v1,v2), m3(v3){    // some other assignment operation}
成员变量的初始化顺序与声明的顺序相关,与在初始化列表中的顺序无关。初始化列表先于构造函数的函数体执行。
#include "iostream"using namespace std;class ABC{public:    ABC(int a, int b, int c)    {        this->a = a;        this->b = b;        this->c = c;        printf("a:%d,b:%d,c:%d \n", a, b, c);        printf("ABC construct ..\n");    }    ~ABC()    {        printf("a:%d,b:%d,c:%d \n", a, b, c);        printf("~ABC() ..\n");    }protected:private:    int a;    int b;    int c;};class MyD{public:    MyD():abc1(1,2,3),abc2(4,5,6),m(100)    //MyD()    {        cout<<"MyD()"<<endl;    }    ~MyD()    {        cout<<"~MyD()"<<endl;    }protected:private:    ABC abc1; // C++编译器不知道如何构造abc1    ABC abc2;    const int m;};int main(){    MyD myD;    return 0;}

2.2.5 对象的动态建立和释放

在编写程序过程中,常常需要动态地分配和撤销内存空间,例如对动态链表中结点的插入与删除。在C语言中是利用库函数malloc()free()来分配和撤销内存空间的。C++提供了较简便而功能较强的运算符newdelete来取代malloc()free()函数。
注意: new和delete是运算符,不是函数,因此执行效率较高。
虽然为了与C语言兼容,C++仍保留malloc()free()函数,但建议用户不要使用malloc()free()函数,而是使用newdelete运算符。
new int;        // 开辟一个存放整数的存储空间,返回一个指向该存储空间的地址(即指针)new int(100);   // 开辟一个存放整数的空间,并指定该整数的初值为100,返回一个指向该存储空间的地址new char[10];   // 开辟一个存放字符数组(包括10个元素)的空间,返回首元素的地址new int[5][4];  // 开辟一个存放二维整型数组(大小为5*4)的空间,返回首元素的地址float *p = new float (3.14159);  //开辟一个存放单精度数的空间,并指定该实数的初值为3.14159,将返回的该空间的地址赋给指针变量p
new 运算符动态地从堆中分配一块「类型」大小的内存,返回其首地址,使用形式:
  • 指针变量 = new 类型(常量);
  • 指针变量 = new 类型 [表达式];
常量是初始化值,可缺省。用new分配数组空间时不能指定初始值。如果由于内存不足等原因而无法正常分配空间,则new会返回一个空指针NULL,用户可以根据该指针的值判断分配空间是否成功。
delete运算符释放已分配的内存空间:
  • delete 指针变量;
  • delete[] 指针变量;
「指针变量」必须是一个new返回的指针。

2.2.6 静态成员变量和静态成员函数

关键字static可以用于说明一个类的成员变量,静态成员变量提供了一个同类对象的共享机制,把一个类的成员变量说明为static时,这个类无论有多少个对象被创建,这些对象共享这个static成员变量,静态成员变量局部于类,它不是对象成员变量。
image-20210308164827128
#include<iostream>using namespace std;class counter{    static int num;     // 声明与定义静态数据成员public:    void setnum(int i)    { num = i; }        // 成员函数访问静态数据成员    void shownum()    { cout << num << '\t'; }};int counter::num = 0;   // 声明与定义静态数据成员,访问静态成员变量方法1int main(){    counter a, b;    a.shownum();        // 调用成员函数访问私有静态数据成员    b.shownum();    a.setnum(10);    a.shownum();    b.shownum();    cout<<a.num<<endl;  // 访问静态成员变量方法2    return 0;}
0   0   10  10
static关键字还可以冠以成员函数,称为静态成员函数(常成员函数)。静态成员函数提供不依赖于类数据结构的共同操作,它没有this指针。在类外调用静态成员函数用类名:: 作限定词,或通过对象调用。
image-20210308165544271
静态成员函数中,不能使用普通成员变量。同理,普通成员函数中也不能使用静态成员变量。因为静态成员变量是属于整个类的,编译器分不清楚是哪个具体对象的。

2.2.7 有元

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
友元函数在函数前面用friend关键字声明:
#include <iostream>using namespace std;class Test{    int var;public:    friend void printVar(Test test);    void setVar(int var);};void Test::setVar(int var){    this->var = var;}// 注意:printVar()不是任何类的成员函数void printVar(Test test){    // 因为printVar()是Test的友元,它可以直接访问该类的任何成员    cout << test.var <<endl;}int main(){    Test test;    test.setVar(10);    printVar(test);     // 10    return 0;}
友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

2.2.8 运算符重载

运算符重载使得用户自定义数据以一种更高效的方式工作,例如:
#include <iostream>using namespace std;class Integer{    int var;public:    Integer(int a)    {        var = a;    }    void add(Integer i)    {        var = i.var;    }};int main(){    Integer I1(1);    Integer I2(2);    I1.add(I2);    return 0;}
能不能通过+号来实现函数add()的功能:I1 + I2
自定义运算符操作,可以通过重载运算符来实现。实际上,我们已经在不知不觉之中使用了运算符重载。我们都已习惯于用加法运算符+对整数、单精度数和双精度数进行加法运算,如5+8、 5.8 + 3.67等,其实计算机对整数、单精度数和双精度数的加法操作过程是很不相同的, 但由于C++已经对运算符+进行了重载,所以就能适用于intfloat,、double类型的运算。
重载的运算符是带有特殊名称的函数,函数名是由关键字operator和其后要重载的运算符符号构成的。与其他函数一样,重载运算符有一个返回类型和一个参数列表:
#include <iostream>using namespace std;class Integer{    int var;public:    Integer(int a)    {        var = a;    }    void add(Integer i)    {        var = i.var;    }    void print()    {        cout << var << endl;    }    // 重载+号运算符,自定义操作    Integer operator+(const Integer &a)    {        return Integer(this->var + a.var);    }};int main(){    Integer I1(1);    Integer I2(2);    //I1.add(I2);    Integer I3 = I1 + I2;   // I3 = 3,使用重载的运算符    I3.print();    return 0;}
可重载的运算符列表:
不可重载的运算符:
  • .:成员访问运算符
  • .*, >*:成员指针访问运算符
  • :::域运算符
  • sizeof:长度运算符
  • ?::三目条件运算符
  • #: 预处理符号
重载运算符可以对运算符作出新的解释,但原有的语义不变:
  • 不改变运算符的优先级
  • 不改变运算符的结合性
  • 不改变运算符所需要的操作数
  • 不能创建新的运算符
还可以通过重载<<来输出自定义类,运算符的左右操作数类型不同的情况下,也可以使用有元函数来重载运算符:
#include <iostream>using namespace std;class Integer{    int var;public:    Integer(int a)    {        var = a;    }    friend ostream &operator<<(ostream &output, Integer &i)    {        return output << i.var;    }};int main(){    Integer I1(10);    cout << I1 << endl;     // 10    return 0;}
在第一个参数需要隐式转换的情形下,使用友员函数重载运算符是正确的选择,友员函数没有 this指针,所需操作数都必须在参数表显式声明,很容易实现类型的隐式转换。C++中不能用友员函数重载的运算符有=()[]->
为什么不要重载&&||操作符:
  • &&||是C++中非常特殊的操作符
  • &&||内置实现了短路规则
  • 操作符重载是靠函数重载来完成的,操作数作为函数参数传递,C++的函数参数都会被求值,无法实现短路规则

2.3 继承和派生

面向对象程序设计中最重要的一个概念是继承。继承允许我们依据另一个类来定义一个类,这使得创建和维护一个应用程序变得更容易。这样做,也达到了重用代码功能和提高执行效率的效果。万物中皆有继承,继承是重要的现象:
image-20210309162516424
当创建一个类时,有时不需要重新编写新的数据成员和成员函数,只需指定新建的类继承了一个已有的类的成员即可。这个已有的类称为基类(父类),新建的类称为派生类(子类)
继承代表了 is a 关系。例如,哺乳动物是动物,狗是哺乳动物,因此,狗是动物,等等。
img

2.3.1 派生类的访问控制

  • 子类拥有父类的所有成员变量和成员函数
  • 子类可以拥有父类没有的方法和属性
  • 子类就是一种特殊的父类
  • 子类对象可以当作父类对象使用
// 基类class Animal {    // eat() 函数    // sleep() 函数};//派生类class Dog : public Animal {    // bark() 函数};
派生类继承了基类的全部成员变量和成员方法(除了构造和析构之外的成员方法),这些成员的访问属性,在派生过程中是可以调整的。不同的继承方式会改变继承成员的访问属性:
  • public继承:父类成员在子类中保持原有访问级别
  • private继承:父类成员在子类中变为private成员
  • protected继承:父类中public成员会变成protected、父类中protected成员仍然为protected、父类中private成员仍然为private
private成员在子类中依然存在,但是却无法访问到。不论何种方式继承基类,派生类都不能直接使用基类的私有成员。

2.3.2 继承中的构造析构调用

继承中的构造析构调用原则:
  • 子类对象在创建时会首先调用父类的构造函数
  • 父类构造函数执行结束后,执行子类的构造函数
  • 当父类的构造函数有参数时,需要在子类的初始化列表中显示调用
  • 析构函数调用的先后顺序与构造函数相反
总的来说是先构造父类,再构造成员变量、最后构造自己。先析构自己,在析构成员变量、最后析构父类。所以先构造的对象,后释放。

2.3.3 继承中的同名成员

当子类成员变量与父类成员变量同名时,子类依然从父类继承同名成员。需要在子类中通过作用域符::进行同名成员区分(在派生类中使用基类的同名成员,显式地使用类名限定符)。同名成员存储在内存中的不同位置。
class B: public A {public:    void print();    void printAB()    {        A::print();        print();    }};

2.3.4 继承中的static关键字

  • 基类定义的静态成员,将被所有派生类共享
  • 根据静态成员自身的访问特性和派生类的继承方式,在类层次体系中具有不同的访问性质 (依然遵守派生类的访问控制
  • 派生类中访问静态成员,用显式说明:类名::成员或通过对象访问:对象名.成员
static不但要初始化,更重要的显示地告诉编译器分配内存。

2.3.5 多继承

C++中允许多继承,一个类可以继承自多个类,拥有多个类的成员变量和成员函数:
class A: public B, public C{    // ...};
  • 多个基类的派生类构造函数可以用初始式调用基类构造函数初始化数据成员
  • 执行顺序与单继承构造函数情况类似。多个直接基类构造函数执行顺序取决于定义派生类时指定的各个继承基类的顺序
  • 一个派生类对象拥有多个直接或间接基类的成员。不同名成员访问不会出现二义性。如果不同的基类有同名成员,派生类对象访问时应该加以识别。

2.3.6 虚继承

如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性。
image-20210309170920489
image-20210309170934851
  • 如果一个派生类从多个基类派生,而这些基类又有一个共同的基类,则在对该基类中声明的名字进行访问时,可能产生二义性
  • 如果在多条继承路径上有一个公共的基类,那么在继承路径的某处汇合点,这个公共基类就会在派生类的对象中产生多个基类子对象
  • 要使这个公共基类在派生类中只产生一个子对象,必须对这个基类声明为虚继承,使这个基类成为虚基类
  • 虚继承声明使用关键字virtual
image-20210309171426806
image-20210309171435086
  • 继承是面向对象程序设计实现软件重用的重要方法。程序员可以在已有基类的基础上定义新的派生类。
  • 单继承的派生类只有一个基类。多继承的派生类有多个基类。
  • 派生类对基类成员的访问由继承方式和成员性质决定。
  • 创建派生类对象时,先调用基类构造函数初始化派生类中的基类成员。调用析构函数的次序和调用构造函数的次序相反。
  • C++提供虚继承机制,防止类继承关系中成员访问的二义性。多继承提供了软件重用的强大功能,但也增加了程序的复杂性。

2.4 多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

2.4.1 C++中的多态

C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。如果父类指针指向的是父类对象则调用父类中定义的函数,如果父类指针指向的是子类对象则调用子类中定义的重写函数。
image-20210309172746663
C++中通过virtual关键字对多态进行支持,使用virtual声明的函数被重写后即可展现多态特性:
#include <iostream>using namespace std;class Parent{public:    // 子类需要重写被virtual关键字修饰的函数    virtual void print()    {        cout << "parent" << endl;    }};class Child : public Parent{public:    // 重写父类被virtual关键字修饰的函数    void print()    {        cout << "child" << endl;    }};int main(){    Parent parent;    Parent *test = &parent;    test->print();  // parent    Child child;    test = &child;  // 指向子类的基类指针    test->print();  // child,发生多态    return 0;}
多态成立的条件:
  • 要有继承
  • 要有函数重写虚函数
  • 要有父类指针(父类引用)指向子类对象
virtual关键字修饰的函数称作虚函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

2.4.2 联编

联编是指一个程序模块、代码之间互相关联的过程,又分为静态联编动态联编
  • 静态联编(Static Binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配(重载函数使用的是静态联编)。
  • 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编、或后期绑定(switch语句和 if语句是动态联编的例子)。
C++与C相同,是静态编译型语言,在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象,所以编译器认为父类指针指向的是父类对象。由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象,从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数,这种特性就是静态联编。

2.4.3 vtable虚函数表和vptr虚函数表指针

C++的动态多态性是通过虚函数来实现的,通过virtual函数,指向子类的基类指针可以调用子类的函数。
虚函数表
当类中声明虚函数时,编译器会在类中生成一个虚函数表(vtable)。虚函数表是一个存储类成员函数指针的数据结构,虚函数表是由编译器自动生成与维护的,每个使用虚函数的类(或者从使用虚函数的类派生出来的子类)都有自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚函数表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数。
虚函数表指针
virtual成员函数会被编译器放入虚函数表中,编译器还会添加一个指向基类的指针,称之为vptrvptr在创建类实例时自动设置,以便指向该类的虚函数表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。这会让编译器为每个类对象的多分配一个vptr指针的大小的空间用于存放在派生类中
image-20210309180837366
vptr指针的存在:
#include <iostream>using namespace std;class A{public:    void printf()    {        cout << "aaa" << endl;    }private:    int a;};class B{public:    virtual void printf()    {        cout << "aaa" << endl;    }private:    int a;};int main(){    // 加上virtual关键字, C++编译器会增加一个指向虚函数表的指针    printf("sizeof(a):%d, sizeof(b):%d \n", sizeof(A), sizeof(B));    return 0;}
sizeof(a):4, sizeof(b):16
对象中的vptr指针什么时候被初始化?
对象在创建的时,由编译器对vptr指针进行初始化。只有当对象的构造完全结束后,vptr的指向才最终确定。父类对象的vptr指向父类虚函数表,子类对象的vptr指向子类虚函数表。
img

2.5 纯虚函数和抽象类

想要在基类中定义虚函数,以便在派生类中重写该函数让其更好地适用于对象,但是有时在基类中又无法对虚函数给出有意义的实现,这个时候就会用到纯虚函数
纯虚函数是一个在基类中说明的虚函数,在基类中并没有定义,但要求任何派生类都要定义自己的版本,纯虚函数为各派生类提供了一个公共的接口:
virtual int print() = 0;
= 0 用来告诉编译器,该函数并没有主体,这样的虚函数就是纯虚函数。一个具有纯虚函数的基类称为抽象类。抽象类只能作为基类来派生新类使用,不能创建抽象类的对象,抽象类的指针和引用->由抽象类派生出来的类的对象。
在抽象类中,在成员函数内可以调用纯虚函数,但在构造函数和析构函数内部不能使用纯虚函数。如果一个类从抽象类派生而来,它必须实现了基类中的所有纯虚函数,才能成为非抽象类。如果在派生类中没有全部覆盖基类的纯虚函数,那么派生类也会变成抽象类,不能创建对象。
抽象类可以有构造函数,但构造函数不能是虚函数。当基类指针所指向派生类对象时,删除对象后,我们可能希望调用适当的析构函数。如果析构函数不是虚拟的,则只能调用基类析构函数。所以析构函数可以是虚析构函数。每个派生出来的子类可以实现自己的析构函数,以便在多态中删除对象后可以正确的析构。

3. 泛型编程与模板

泛型编程这个概念最早就是来源于C++当初设计STL时所引入的模板(Template),是指独立与任何类型的方式编写代码。泛型编程和面向对象编程,都依赖与某种形式的多态。面向对象编程的多态性在运行时应用于存在继承关系的类,一段代码可以可以忽略基类和派生类之间的差异。在泛型编程中,编写的代码可以用作多种类型的对象。面向对象编程所依赖的多态性称为运行时多态性,泛型编程所依赖的多态性称为编译时多态性或参数式多态性。

3.1 函数模板

C++提供了函数模板(FunctionTemplate)。所谓函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体指定,用一个虚拟的类型来代表。这个通用函数就称为函数模板。凡是函数体相同的函数都可以用这个模板来代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现了不同函数的功能。
函数模板声明:
template <typename T> void mySwap(T& a, T& b){    T temp;    temp = a;    a = b;    b = temp;}
其中typename关键字也可以用class代替,<>中的参数称为模板形参,模板形参不能为空。一但声明了模板函数就可以用模板函数的形参名来声明类中的成员变量和成员函数,即可以在该函数中使用内置类型的任何地方使用模板形参名。
int main(){    int a = 1;    char b = 'b';    mySwap<int>(a, b);    return 0;}
调用时也可以省略<>,让编译器自动推断:
mySwap(a, b);
但函数模板不提供隐式的数据类型转换 必须是严格类型匹配。当函数模板和普通函数都符合调用时,优先调用的是普通函数,如果函数模板的传参产生了更好的匹配,则会调用函数模板。若要显示地使用函数模板,可以使用<>类型列表表示:
mySwap<>(a, b);
编译器并不是把函数模板处理成能够处理任意类的函数,只是从函数模板通过具体类型产生不同的函数,编译器会对函数模板进行两次编译,第一次在声明的地方对模板代码本身进行编译;第二次在调用的地方对参数替换后的代码进行编译。

3.2 类模板

类模板与函数模板的定义和使用类似,在类实例化和被继承的时候就要显示指定模板类型:
template<typename T> class TClass{    // ...};int main(){    TClass<int> t;    return 0;}
一个用类模板实现的栈:
#include <iostream>#include <vector>#include <cstdlib>#include <string>#include <stdexcept>using namespace std;template <class T>class Stack {  private:    vector<T> elems;     // 元素  public:    void push(T const&);  // 入栈    void pop();               // 出栈    T top() const;            // 返回栈顶元素    bool empty() const{       // 如果为空则返回真。        return elems.empty();    }};template <class T>void Stack<T>::push (T const& elem){    // 追加传入元素的副本    elems.push_back(elem);}template <class T>void Stack<T>::pop (){    if (elems.empty()) {        throw out_of_range("Stack<>::pop(): empty stack");    }    // 删除最后一个元素    elems.pop_back();}template <class T>T Stack<T>::top () const{    if (elems.empty()) {        throw out_of_range("Stack<>::top(): empty stack");    }    // 返回最后一个元素的副本    return elems.back();}int main(){    try {        Stack<int>         intStack;  // int 类型的栈        Stack<string> stringStack;    // string 类型的栈        // 操作 int 类型的栈        intStack.push(7);        cout << intStack.top() <<endl;        // 操作 string 类型的栈        stringStack.push("hello");        cout << stringStack.top() << std::endl;        stringStack.pop();        stringStack.pop();    }    catch (exception const& ex) {        cerr << "Exception: " << ex.what() <<endl;        return -1;    }}

4. 异常处理

异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。
异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:trycatchthrow
  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch关键字用于捕获异常。
  • trytry 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个catch块。
如果有一个块抛出一个异常,捕获异常的方法会使用trycatch关键字。try块中放置可能抛出异常的代码,try块中的代码被称为保护代码。使用try/catch语句的语法如下所示:
try{   // 受异常处理代码}catch( ExceptionName e1 ){   // catch 块}catch( ExceptionName e2 ){   // catch 块}catch( ExceptionName eN ){   // catch 块}
如果try 块在不同的情境下会抛出不同的异常,这个时候可以尝试罗列多个catch语句,用于捕获不同类型的异常。

4.1 抛出异常

可以使用throw语句在代码块中的任何地方抛出异常。throw语句的操作数可以是任意的表达式,表达式的结果的类型决定了抛出的异常的类型。
以下是尝试除以零时抛出异常的实例:
double division(int a, int b){   if( b == 0 )   {      throw "Division by zero condition!";   }   return (a/b);}

4.2 捕获异常

catch块跟在try块后面,用于捕获异常。您可以指定想要捕捉的异常类型,这是由catch关键字后的括号内的异常声明决定的。
try{   // 受异常处理代码}catch( ExceptionName e ){  // 处理 ExceptionName 异常的代码}
上面的代码会捕获一个类型为ExceptionName的异常。如果想让catch块能够处理try块抛出的任何类型的异常,则必须在异常声明的括号内使用省略号...,如下所示:
try{   // 受异常处理代码}catch(...){  // 能处理任何异常的代码}

4.3 标准异常

C++ 提供了一系列标准的异常,定义在<exception>中,可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:
C++ 异常的层次结构

4.4 自定义异常

可以通过继承和重载exception类来定义新的异常。下面的实例演示了如何使用std::exception类来实现自己的异常:
#include <iostream>#include <exception>using namespace std;struct MyException : public exception{  const char * what () const throw ()  {    return "C++ Exception";  }};int main(){  try  {    throw MyException();  }  catch(MyException& e)  {    std::cout << "MyException caught" << std::endl;    std::cout << e.what() << std::endl;  }  catch(std::exception& e)  {    //其他的错误  }}

5. 文件和流

5.1 I/O流概念和流类库结构

程序的输入指的是从输入文件将数据传送给程序,程序的输出指的是从程序将数据传送给输出文件。
C++输入输出包含以下三个方面的内容:
  • 对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准的输入输出,简称标准I/O
  • 以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件。以外存文件为对象的输入输出称为文件的输入输出,简称文件I/O
  • 对内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息)。这种输入和输出称为字符串输入输出,简称串I/O

5.1.1 C++与C的I/O类型安全和可扩展性

在C语言中,用printfscanf进行输入输出,往往不能保证所输入输出的数据是可靠的安全的。在C++的输入输出中,编译系统对数据类型进行严格的检查,凡是类型不正确的数据都不可能通过编译。因此C++的I/O操作是类型安全(Type Safe)的。C++的I/O操作是可扩展的,不仅可以用来输入输出标准类型的数据,也可以用于用户自定义类型的数据。
C++通过I/O类库来实现丰富的I/O功能。这样使C++的输人输出明显地优于C 语言中的printfscanf,但是也为之付出了代价,C++的I/O系统变得比较复杂,要掌握许多细节。
C++编译系统提供了用于输入输出的iostream类库。iostream这个单词是由3个部分组成的,即iostream,意为输入输出流。在iostream类库中包含许多用于输入输出的 类。常用的见表:
image-20210312174828193
image-20210312174839569
ios是抽象基类,由它派生出istream类和ostream类,两个类名中第1个字母io分别代表输入(Input)输出(Output)istream类支持输入操作,ostream类支持输出操作, iostream类支持输入输出操作。iostream类是从istream类和ostream类通过多重继承而派生的类。其继承层次见上图表示。
C++对文件的输入输出需要用ifstrcamofstream类,两个类名中第1个字母io分别代表输入和输出,第2个字母f代表文件 (File)ifstream支持对文件的输入操作, ofstream支持对文件的输出操作。类ifstream继承了类istream,类ofstream继承了类ostream,类fstream继承了类iostream。见图 :
image-20210312175021435

5.1.2 与iostream类库有关的头文件

iostream类库中不同的类的声明被放在不同的头文件中,用户在自己的程序中用#include命令包含了有关的头文件就相当于在本程序中声明了所需要用到的类。可以换 —种说法:头文件是程序与类库的接口,iostream类库的接口分别由不同的头文件来实现。常用的有 :
  • iostream 包含了对输入输出流进行操作所需的基本信息
  • fstream 用于用户管理的文件的I/O操作
  • strstream 用于字符串流I/O
  • stdiostream 用于混合使用C和C + +的I/O机制时,例如想将C程序转变为C++程序
  • iomanip 在使用格式化I/O时应包含此头文件

5.1.3 在iostream头文件中定义的流对象

iostream头文件中定义的类有iosistreamostreamiostreamistream_withassignostream_withassigniostream_withassign等。
iostream头文件中不仅定义了有关的类,还定义了4种流对象:
iostream头文件中定义以上4个流对象用以下的形式(以cout为例):
ostream cout (stdout);
在定义coutostream流类对象时,把标准输出设备stdout作为参数,这样它就与标准输出设备(显示器)联系起来,如果有
cout << 3;
就会在显示器的屏幕上输出3。

5.1.4 在iostream头文件中重载运算符

<<>>本来在C++中是被定义为左位移运算符和右位移运算符的,由于在iostream头文件中对它们进行了重载, 使它们能用作标准类型数据的输入和输出运算符。所以,在用它们的程序中必须用#include命令把iostream包含到程序中。
  • >> a表示将数据放入a对象中
  • << a表示将a对象中存储的数据拿出

5.2 标准I/O流

5.2.1 cout流对象

cont是Console Output的缩写,意为在控制台(终端显示器)的输出。
cout不是C++预定义的关键字,它是ostream流类的对象,在iostream中定义。 顾名思义,流是流动的数据,cout流是流向显示器的数据。cout流中的数据是用流插入运算符<<顺序加入的。如果有:
 cout<<"I "<<"study C++ "<<"very hard. << “wang bao ming ";
按顺序将字符串Istudy C++very hard.插人到cout流中,cout就将它们送到显示器,在显示器上输出字符串I study C++ very hard.cout流是容纳数据的载体,它并不是一个运算符。
cout <<输出基本类型的数据时,可以不必考虑数据是什么类型,系统会判断数据的类型,并根据其类型选择调用与之匹配的运算符重载函数。这个过程都是自动的,用户不必干预。如果在C语言中用printf函数输出不同类型的数据,必须分别指定相应的输出格式符,十分麻烦,而且容易出错。C++的I/O机制对用户来说,显然是方便而安全的。
cout流在内存中对应开辟了一个缓冲区,用来存放流中的数据,当向cout流插人一个endl时,不论缓冲区是否已满,都立即输出流中所有数据,然后插入一个换行符, 并刷新流(清空缓冲区)。注意如果插人一个换行符\n(如cout << a << "\n"),则只输出和换行,而不刷新cout流(但并不是所有编译系统都体现出这一区别)。
iostream中只对<<>>运算符用于标准类型数据的输入输出进行了重载,但未对用户声明的类型数据的输入输出进行重载。如果用户声明了新的类型,并希望用<<>>运算符对其进行输入输出,就需要重载运算符。

5.2.2 cerr流对象

cerr流对象是标准错误流,cerr流已被指定为与显示器关联。cerr的作用是向标准错误设备(Standard Error Device)输出有关出错信息。cerr与标准输出流cout的作用和用法差不多。但有一点不同:cout流通常是传送到显示器输出,但也可以被重定向输出到磁盘文件,而cerr流中的信息只能在显示器输出。当调试程序时,往往不希望程序运行时的出错信息被送到其他文件,而要求在显示器上及时输出,这时应该用cerrcerr流中的信息是用户根据需要指定的。

5.2.3 clog流对象

clog流对象也是标准错误流,它是Console Log的缩写。它的作用和cerr相同,都是在终端显示器上显示出错信息。区别:cerr是不经过缓冲区,直接向显示器上输出有关信息,而clog中的信息存放在缓冲区中,缓冲区满后或遇endl时向显示器输出。

5.2.4 缓冲区的概念

image-20210312181251408

5.2.5 标准输入流

标准输入流对象:
  • cin.get()一次只能读取一个字符
  • cin.get(一个参数)读一个字符
  • cin.get(三个参数)可以读字符串
  • cin.getline()
  • cin.ignore()
  • cin.peek()
  • cin.putback()

5.2.6 标准输出流

标准输出流对象:
  • cout.flush()
  • cout.put()
  • cout.write()
  • cout.width()
  • cout.fill()
  • cout.setf()

5.3 文件I/O

5.3.1 打开文件

在从文件读取信息或者向文件写入信息之前,必须先打开文件。ofstreamfstream对象都可以用来打开文件进行写操作,如果只需要打开文件进行读操作,则使用ifstream对象。
下面是open()函数的标准语法,open()函数是fstreamifstreamofstream对象的一个成员。
void open(const char *filename, ios::openmode mode);
在这里,open()成员函数的第一参数指定要打开的文件的名称和位置,第二个参数定义文件被打开的模式。
可以把以上两种或两种以上的模式结合使用。例如,如果您想要以写入模式打开文件,并希望截断文件,以防文件已存在,那么您可以使用下面的语法:
ofstream outfile;outfile.open("file.dat", ios::out | ios::trunc );
类似地,如果想要打开一个文件用于读写,可以使用下面的语法:
ifstream  afile;afile.open("file.dat", ios::out | ios::in );

5.3.2 关闭文件

当C++程序终止时,它会自动关闭刷新所有流,释放所有分配的内存,并关闭所有打开的文件。但应该养成一个好习惯,在程序终止前关闭所有打开的文件。
下面是close()函数的标准语法,close()函数是fstreamifstreamofstream对象的一个成员。
void close();

5.3.3 写入文件

在C++中,使用流插入运算符<<向文件写入信息,就像使用该运算符输出信息到屏幕上一样。唯一不同的是,在这里使用的是ofstreamfstream对象,而不是cout对象。

5.3.4 读取文件

在C++中,使用流提取运算符>>从文件读取信息,就像使用该运算符从键盘输入信息一样。唯一不同的是,在这里您使用的是ifstreamfstream对象,而不是cin对象。

References

 

Nobelium is built with ♥ and ⚛ Next.js. Proudly deployed on ▲Vercel.

© Ashinch 2021 桂ICP备18011166号-1