系列文章目录
第一章 C语言基础知识
第二章 C语言高级编程
第三章 C语言数据结构
第四章 C语言高级数据结构
第五章 C++核心编程
前言
c++语言在c语言的基础上添加了面向对象编程和泛型编程的支持。c++继承了c语言高效,简洁,快速和可移植的传统。c++融合了3种不同的编程方式:c语言代表的过程性语言,c++在c语言基础上添加的类代表的面向对象语言,c++模板支持的泛型编程。c++98标准,它不仅描述了已有的c++特性,还对语言进行了扩展,添加了异常、运行阶段类型识别(RTTI)、模板和标准模板库(STL)。c++不断发展,IOS标准委员会于2011年8月批准了新标准ISO/IEC 14882:2011,该标准被称为c++11,与c++98一样c++11也新增了许多特性。
一、C++初识
- c++头文件没有扩展名。但是有些c语言的头文件被转换为c++的头文件,这些文件被重新命名,丢掉了扩展名.h(使之成为c++风格头文件),并在文件名称前面加上前缀c(表明来自c语言)。例如c++版本的math.h为cmath。
- namespace是指标识符的各种可见范围。命名空间用关键字namespace 来定义。命名空间是C++的一种机制,用来把单个标识符下的大量有逻辑联系的程序实体组合到一起。此标识符作为此组群的名字。
- cout是c++中的标准输出流,endl是输出换行并刷新缓冲区。
- 面向过程是通过分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。面向过程编程思想的核心:功能分解,自顶向下,逐层细化(程序=数据结构+算法)。缺点是不符合人的思维习惯,而是要用计算机的思维方式去处理问题,而且面向过程编程语言重用性低,维护困难。
- 面向对象中,算法与数据结构被看做是一个整体,称作对象,现实世界中任何类的对象都具有一定的属性和操作,也总能用数据结构与算法两者合一地来描述(程序 = 对象 + 对象 + ……)。程序就是许多对象在计算机中相继表现自己,而对象则是一个个程序实体。 面向对象编程思想的核心:应对变化,提高复用。使用面向对象技术,常常要使用许多代码模块,每个模块都只提供特定的功能,它们是彼此独立的,这样就增大了代码重用的几率,更加有利于软件的开发、维护和升级。
- 面向对象三大特性:
- 封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。类将成员变量和成员函数封装在类的内部,根据需要设置访问权限,通过成员函数管理内部状态。
- 继承:继承所表达的是类之间相关的关系,这种关系使得对象可以继承另外一类对象的特征和能力。避免公用代码的重复开发,减少代码和数据冗余。
- 多态:多态性可以简单地概括为“一个接口,多种方法”,字面意思为多种形态。程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。
二、C++对C的扩展
- ::作用域运算符可以用来解决局部变量与全局变量的重名问题 ,即在局部变量的作用域内,可用::对被屏蔽的同名的全局变量进行访问。
- 名字控制。创建名字是程序设计过程中一项最基本的活动,当一个项目很大时,它会不可避免地包含大量名字。c++允许我们对名字的产生和名字的可见性进行控制。
- 在c++中,名称(name)可以是符号常量、变量、函数、结构、枚举、类和对象等等。工程越大,名称互相冲突性的可能性越大。标准C++引入关键字namespace(命名空间/名字空间/名称空间),可以更好地控制标识符的作用域。
- 命名空间只能全局范围内定义;可以嵌套另一个命名空间;命名空间是开放的,即可以随时把新的成员加入已有的命名空间中;声明和实现可分离;命名空间可以起别名。
- 无名命名空间,意味着命名空间中的标识符只能在本文件内访问,相当于给这个标识符加上了static,使得其可以作为内部连接。
- 使用using声明或using编译指令会增加命名冲突的可能性。也就是说,如果有名称空间,并在代码中使用作用域解析运算符,则不会出现二义性。
- 全局变量检测增强。int a;int a = 10;在c++下编译失败。
- C++中所有的变量和函数都必须有类型。C语言中,int fun() 表示返回值为int,接受任意参数的函数,int fun(void) 表示返回值为int的无参函数。C++中都表示返回值为int的无参函数。
- 更严格的类型转换。在C++,不同类型的变量一般是不能直接赋值的,需要相应的强转。
- struct类型加强。c++中的结构体不需要加上struct关键字且既可以定义成员变量,也可以定义成员函数。
- “新增”bool类型关键字。标准c++的bool类型有两种内建的常量true(转换为整数1)和false(转换为整数0)表示状态。bool类型占1个字节大小,给bool类型赋值时,非0值会自动转换为true(1),0值会自动转换false(0)。c语言中c99标准已经有bool类型,包含头文件stdbool.h,就可以使用和c++一样的bool类型。
- 三目运算符功能增强。c语言三目运算表达式返回值为数据值,为右值,不能赋值。c++语言三目运算表达式返回值为变量本身(引用),为左值,可以赋值。
左值为Lvalue,L代表Location,表示内存可以寻址,可以赋值。右值为Rvalue,R代表Read,就是可以知道它的值。比如:int temp = 10; temp在内存中有地址,10没有,但是可以Read到它的值。 - C/C++中的const。c中认为const应该是一个只读变量,既然是变量那么就会给const分配内存。在c++中,一个const不必创建内存空间,是否为const常量分配内存空间依赖于如何使用。但取一个const地址, 或者把它定义为extern,则会为该const创建内存空间。
- c语言全局const会被存储到只读数据段,不可直接修改,通过指针间接修改编译通过,运行失败。局部const存储在堆栈区,不可直接修改,但能通过指针间接修改。
- c++全局const与c语言一致。但对于局部的const变量要区别对待,对于基础数据类型,编译器会把它放到符号表中,不分配内存,当对其取地址时,会分配内存,如果用一个变量初始化const变量,也会分配内存。对于自定数据类型,比如类对象,那么也会分配内存。分配了内存,就可以通过指针的间接赋值修改。
- c中const默认为外部连接,c++中const默认为内部连接。当c语言两个文件中都有const int a的时候,编译器会报重定义的错误。而在c++中,则不会,因为c++中的const默认是内部连接的。如果想让c++中的const具有外部连接,必须显示声明为: extern const int a = 10;
- 尽量以const替换#define。因为const有类型,可进行编译器类型安全检查。#define无类型,不可进行类型检查。const有作用域,而#define不重视作用域,默认定义处到文件结尾。
- 引用。变量名实质上是一段连续内存空间的别名,是一个标号,程序中通过变量来申请并命名内存空间,通过变量的名字可以使用存储空间,引用可以作为一个已定义变量的别名。
- 必须在声明引用变量时进行初始化。引用初始化之后不能改变。不能有NULL引用,必须确保引用是和一块合法的存储单元关联。可以建立对数组的引用。
- 当引用被用作函数参数的时,在函数内对任何引用的修改,将对还函数外的参数产生改变。如果从函数中返回一个引用,必须像从函数中返回一个指针一样对待。当函数返回值时,引用关联的内存一定要存在。通过引用参数产生的效果同按地址传递是一样的。引用的语法更清楚简单,C++主张用引用传递取代地址传递的方式,因为引用语法容易且不易出错。不能返回局部变量的引用。函数当左值,必须返回引用。
- 引用的本质在c++内部实现是一个指针常量。 Type& ref = val; // Type* const ref = &val;**
- 函数参数变成指针的引用,用不着取得指针的地址。
- 常量引用主要用在函数的形参,尤其是类的拷贝/复制构造函数。引用不产生新的变量,减少形参与实参传递时的开销。由于引用可能导致实参随形参改变而改变,将其定义为常量引用可以消除这种副作用。
- 可以把一个字面量赋给常引用。const int& ref = 100; //int temp = 200; const int& ret = temp;
- 内联函数。c++出现之后,使用预处理宏会出现两个问题:宏看起来像一个函数调用,但是会有隐藏一些难以发现的错误。预处理器宏不能用作类类的成员函数。为了保持预处理宏的效率又增加安全性,而且还能像一般成员函数那样可以在类里访问自如,c++引入了内联函数(inline function)。
- 内联函数为了继承宏函数的效率,没有函数调用时开销,然后又可以像普通函数那样,可以进行参数,返回值类型的安全检查,又可以作为成员函数。在普通函数(非成员函数)函数前面加上inline关键字使之成为内联函数。但是必须注意必须函数体和声明结合在一起,否则编译器将它作为普通函数来对待。
- 内联函数相对于普通函数的优势只是省去了函数调用时候的压栈,跳转,返回的开销。我们可以理解为内联函数是以空间换时间。
- 任何在类内部定义的函数自动成为内联函数。
- 内联函数并不是何时何地都有效,内联仅仅只是给编译器一个建议,编译器不一定会接受这种建议,如果你没有将函数声明为内联函数,那么编译器也可能将此函数做内联编译。一个好的编译器将会内联小的、简单的函数。
- 以下情况编译器可能考虑不会将函数进行内联编译:不能存在任何形式的循环语句;不能存在过多的条件判断语句;函数体不能过于庞大;不能对函数进行取址操作。
- 函数的默认参数。c++在声明函数原型的时可为一个或者多个参数指定默认(缺省)的参数值,当函数调用的时候如果没有指定这个值,编译器会自动用默认值代替。函数的默认参数从左向右,如果一个参数设置了默认参数,那么这个参数之后的参数都必须设置默认参数。如果函数声明和函数定义分开写,函数声明和函数定义不能同时设置默认参数。
- c++在声明函数时,可以设置占位参数。占位参数只有参数类型声明,而没有参数名声明。一般情况下,在函数体内部无法使用占位参数。占位参数也可以设置默认值,占位参数也是参数,必须传参数。操作符重载的后置++会使用。
- 函数重载。同一个函数名在不同场景下可以具有不同的含义。在传统c语言中,函数名必须是唯一的,程序中不*允许出现同名的函数。在c++中是允许出现同名的函数,这种现象称为函数重载。函数重载的目的就是为了方便的使用函数名。
- 实现函数重载的条件:同一个作用域;参数个数不同;参数类型不同;参数顺序不同。返回值不作为函数重载依据。
- 函数重载和默认参数一起使用,需要额外注意二义性问题的产生。
- 函数重载实现原理:编译器用不同的参数类型来修饰不同的函数名,比如void func(); 编译器可能会将函数名修饰成_func,当编译器碰到void func(int x),编译器可能将函数名修饰为_func_int,当编译器碰到void func(int x,char c),编译器可能会将函数名修饰为_func_int_char。
- c++中需要支持函数重载,所以c和c++中对同一个函数经过编译后生成的函数名是不相同的,这就导致了一个问题,如果在c++中调用一个使用c语言编写模块中的某个函数,那么c++是根据c++的名称修饰方式来查找并链接这个函数,那么就会发生链接错误。
- extern "C"的主要作用就是为了实现c++代码能够调用其他c语言代码。加上extern "C"后,这部分代码编译器按c语言的方式进行编译和链接,而不是按c++的方式。有两种方式实现extern “C”,一种extern “C” void show();另一种是在C语言的头文件中加入6行代码。
#ifdef __cplusplus extern "C" { #endif #ifdef __cplusplus } #endif
三、类和对象
-
类的封装:封装特性包含两个方面,一个是属性和变量合成一个整体,一个是给属性和函数增加访问权限。把变量(属性)和函数(操作)合成一个整体,封装在一个类中,对变量和函数进行访问控制。将成员变量设置为private, 可赋予客户端访问数据的一致性。可细微划分访问控制,可以实现对成员变量的“不准访问”、“只读访问”、“读写访问”,甚至“只写访问”。
-
面向对象程序设计案例:设计立方体类、点和圆的关系。
- 立方体类:成员变量(private):长、宽、高。成员函数(public):设置长、宽、高,获取长、宽、高,计算立方体面积、体积,比较立方体体积。全局函数:比较立方体体积。
- 点和圆的关系:设计点类,成员变量(private):坐标X和坐标Y。成员函数(public):设置坐标X和坐标Y,获取坐标X和坐标Y。 设计圆类,成员变量(private):圆心和半径。成员函数(public):设置圆心和半径,获取圆心和半径,判断点和圆的关系。圆心由点类的坐标X和坐标Y构造。
-
对象的构造和析构。对象的初始化和清理是两个非常重要的安全问题,一个对象或者变量没有初始时,对其使用后果是未知,同样的使用完一个变量,没有及时清理,也会造成一定的安全问题。c++为了给我们提供这种问题的解决方案,构造函数和析构函数,这两个函数将会被编译器自动调用,完成对象初始化和对象清理工作。
- 构造函数主要作用在于创建对象时为对象的成员属性赋值,构造函数由编译器自动调用,无须手动调用。析构函数主要用于对象销毁前系统自动调用,执行一些清理工作。
- 构造函数按参数类型:分为无参构造函数和有参构造函数。按类型分类:普通构造函数和拷贝构造函数(复制构造函数)。匿名对象,没有名字的对象,由匿名对象的参数类型判断调用哪一个构造函数,不能调用拷贝构造函数去初始化匿名对象。
- 拷贝构造函数的调用时机:对象以值传递的方式传给函数参数,函数局部对象以值传递的方式从函数返回,用一个对象初始化另一个对象。在程序中一个对象的拷贝也是非常耗时的,编译器会对程序执行进行优化,减少一些不必要的拷贝和析构的次数。
- 构造函数调用规则:默认情况下,c++编译器至少为我们写的类增加3个函数,默认构造函数,默认析构函数,默认拷贝构造函数,对类中非静态成员属性简单值拷贝。如果用户定义拷贝构造函数,c++不会再提供任何默认构造函数。如果用户定义了普通构造(非拷贝),c++不在提供默认无参构造,但是会提供默认拷贝构造。
- 深拷贝和浅拷贝:同一类型的对象之间可以赋值,使得两个对象的成员变量的值相同,两个对象仍然是独立的两个对象,这种情况被称为浅拷贝。一般情况下,浅拷贝没有任何副作用,但是当类中有指针,并且指针指向动态分配的内存空间,析构函数做了动态内存释放的处理,会导致内存问题。当类中有指针,并且此指针有动态分配空间,析构函数做了释放处理,往往需要自定义拷贝构造函数,自行给指针动态分配空间,深拷贝。
- 多个对象构造和析构:构造函数和其他函数不同,除了有名字,参数列表,函数体之外还有初始化列表。**初始化成员列表(参数列表)**只能在构造函数使用。类对象作为成员,先调用对象成员的构造函数,再调用本身的构造函数。析构函数和构造函数调用顺序相反,先构造,后析构。
- c++提供了关键字explicit,禁止通过构造函数进行的隐式转换。声明为explicit的构造函数不能在隐式转换中使用,是针对单参数的构造函数(或者除了第一个参数外其余参数都有默认值的多参构造)而言。
- 动态对象创建:c语言提供了动态内存分配(dynamic memory allocation),函数malloc和free可以在运行时从堆中分配存储单元。然而这些函数在c++中不能很好的运行,因为它不能帮我们完成对象的初始化工作。当创建一个c++对象时会发生两件事:为对象分配内存,调用构造函数来初始化那块内存。
- C动态分配内存方法:1) 程序员必须确定对象的长度。2) malloc返回一个void*指针,c++不允许将void*赋值给其他任何指针,必须强转 。3) malloc可能申请内存失败,所以必须判断返回值来确保内存分配成功。4) 用户在使用对象之前必须记住对他初始化,构造函数不能显示调用初始化(构造函数是由编译器调用),用户有可能忘记调用初始化函数。
- C++中解决动态内存分配的方案是把创建一个对象所需要的操作都结合在一个称为new的运算符里。当用new创建一个对象时,它就在堆里为对象分配内存并调用构造函数完成初始化。delete表达式先调用析构函数,然后释放内存。正如new表达式返回一个指向对象的指针一样,delete需要一个对象的地址。
- delete只适用于由new创建的对象。如果使用一个由malloc或者calloc或者realloc创建的对象使用delete,这个行为是未定义的。如果正在删除的对象的指针是NULL,将不发生任何事,因此建议在删除指针后,立即把指针赋值为NULL,以免对它删除两次,对一些对象删除两次可能会产生某些问题。如果对一个void*指针执行delete操作,这将可能成为一个程序错误,除非指针指向的内容是非常简单的,因为它将不执行析构函数。
- 当创建一个对象数组的时候,必须对数组中的每一个对象调用构造函数,除了在栈上可以聚合初始化,必须提供一个默认的构造函数。如果在new表达式中使用[],必须在相应的delete表达式中也使用[]。如果在new表达式中不使用[], 一定不要在相应的delete表达式中使用[]。
- 静态成员:在类定义中,它的成员(包括成员变量和成员函数),这些成员可以用关键字static声明为静态的,称为静态成员。不管这个类创建了多少个对象,静态成员只有一个拷贝,这个拷贝被所有属于这个类的对象共享。
- 在一个类中,若将一个成员变量声明为static,这种成员称为静态成员变量。与一般的数据成员不同,无论建立了多少个对象,都只有一个静态数据的拷贝。静态成员变量,属于某个类,所有对象共享。 静态变量,是在编译阶段就分配空间,对象还没有创建时,就已经分配空间。静态成员变量必须在类中声明,在类外定义。静态数据成员不属于某个对象,在为对象分配空间中不包括静态成员所占空间。静态数据成员可以通过类名或者对象名来引用。
- 在类定义中,前面有static说明的成员函数称为静态成员函数。静态成员函数使用方式和静态变量一样,同样在对象没有创建前,即可通过类名调用。静态成员函数主要为了访问静态变量,但是,不能访问普通成员变量。静态成员函数的意义在于管理静态数据成员,完成对静态数据成员的封装。静态成员函数只能访问静态变量,不能访问普通成员变量。普通成员函数可访问静态成员变量、也可以访问非静态成员变量。
- 如果一个类的成员,既要实现共享,又要实现不可改变,那就用 static const 修饰。定义静态const数据成员时,最好在类内部初始化。
- 静态成员实现单例模式:通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其默认构造函数和拷贝构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。
- 单例模式 – 主席类案例;单例模式 – 打印机案例。
-
C++面向对象模型初探:c++实现了“封装”,数据”和“处理数据的操作(函数)”是分开存储的。c++中的非静态数据成员直接内含在类对象中。成员函数(member function)虽然内含在class声明之内,却不出现在对象中。每一个非内联成员函数(non-inline member function)只会诞生一份函数实例。
- C++类对象中的变量和函数是分开存储。
- c++的数据和操作也是分开存储,并且每一个非内联成员函数只会诞生一份函数实例,也就是说多个同类型的对象会共用一块代码。c++通过提供特殊的对象指针,this指针区分哪个对象调用自己。this指针指向被调用的成员函数所属的对象。
- this指针是隐含在对象成员函数内的一种指针。当一个对象被创建后,它的每一个成员函数都含有一个系统自动生成的隐含指针this,用以保存这个对象的地址,this也称为“指向本对象的指针”,this指针并不是对象的一部分,不会影响sizeof(对象)的结果。This指针是一种隐含指针,它隐含于每个类的非静态成员函数中。静态成员函数内部没有this指针,静态成员函数不能操作非静态成员变量。
- 当形参和成员变量同名时,可用this指针来区分。在类的非静态成员函数中返回对象本身,可使用return *this。
- 用const修饰成员函数时,const修饰this指针指向的内存区域,成员函数体内不可以修改本类中的任何普通成员变量,当成员变量类型符前用mutable修饰时例外。
- 常对象只能调用const的成员函数,常对象可访问 const 或非 const 数据成员,不能修改,除非成员用mutable修饰。
-
友元:类的主要特点之一是数据隐藏,即类的私有成员无法在类的外部(作用域之外)访问。但是,有时候需要在类的外部访问类的私有成员,解决方法是使用友元函数,友元函数是一种特权函数,c++允许这个特权函数访问私有成员。程序员可以把一个全局函数、某个类中的成员函数、甚至整个类声明为友元。
- friend关键字只出现在声明处,其他类、类成员函数、全局函数都可声明为友元,友元函数不是类的成员,不带this指针,友元函数可访问对象任意成员属性,包括私有属性。
- 友元关系不能被继承。友元关系是单向的,类A是类B的朋友,但类B不一定是类A的朋友。友元关系不具有传递性。类B是类A的朋友,类C是类B的朋友,但类C不一定是类A的朋友。
- 实例:编写电视机类,电视机有开机和关机状态,有音量,有频道,提供音量操作的方法,频道操作的方法。由于电视机只能逐一调整频道,不能指定频道,增加遥控类,遥控类除了拥有电视机已有的功能,再增加根据输入调台功能。
- 强化训练:数组类封装。
-
运算符重载。运算符重载,就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型,它只是另一种函数调用的方式。定义很像一个普通的函数定义,只是函数的名字由关键字operator及其紧跟的运算符组成。
-
函数的参数中参数个数取决于两个因素。运算符是一元(一个参数)的还是二元(两个参数);运算符被定义为全局函数(对于一元是一个参数,对于二元是两个参数)还是成员函数(对于一元没有参数,对于二元是一个参数,此时该类的对象用作一个参数)。对于内置的数据类型的表达式的的运算符是不可能改变的。
-
运算符重载碰上友元函数:重载左移操作符(<<),使得cout可以输出对象。
-
可重载的运算符:不能使用C中当前没有意义的运算符(例如用**求幂)不能改变运算符优先级,不能改变运算符的参数个数。
-
自增自减(++/–)运算符重载:++a(前置++),它就调用operator++(a),当编译器看到a++(后置++),它就会去调用operator++(a,int)。如果定义了++c,也要定义c++,对于++和–而言,后置形式是先返回,然后对象++或者–,返回的是对象的原值。前置形式,对象先++或–,返回当前对象,返回的是新对象。调用代码时候,要优先使用前缀形式,除非确实需要后缀形式返回的原值。
-
指针运算符(*、->)重载:智能指针类。
-
赋值(=)运算符重载:’=’ 在编程中是最基本的运算符,可以进行赋值操作,也能引起拷贝构造函数的调用。 如果没有重载赋值运算符,编译器会自动创建默认的赋值运算符重载函数。行为类似默认拷贝构造,进行简单值拷贝。
-
等于和不等于(==、!=)运算符重载:自定义数据类型,编译器不知道如何进行比较。
-
函数调用符号()重载:使用时候很像函数调用,因此称为仿函数。
-
不能重载operator&& 和 operator|| 的原因是,无法在这两种情况下实现内置操作符的完整语义。内置版本的&&和||首先计算左边的表达式,如果这完全能够决定结果,就无需计算右边的表达式了。
-
=, [], () 和 -> 操作符只能通过成员函数进行重载;<< 和 >>只能通过全局函数配合友元函数进行重载;不要重载 && 和 || 操作符,因为无法实现短路规则。
-
强化训练:字符串类封装。
-
-
继承和派生。c++最重要的特征是代码重用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。一个B类继承于A类,或称从类A派生类B。这样的话,类A成为基类(父类), 类B成为派生类(子类)。派生类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员。从基类继承过过来的表现其共性,而新增的成员体现了其个性。
- 派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。
- 继承中的构造和析构:继承中的对象模型,在C++编译器的内部可以理解为结构体,子类是由父类成员叠加子类新成员而成。
- 继承与组合混搭的构造和析构:
- 继承中同名成员的处理方法:当子类成员和父类成员同名时,子类依然从父类继承同名成员。如果子类有成员和父类同名,子类访问其成员默认访问子类的成员(本作用域,就近原则)。在子类通过作用域::进行同名成员区分(在派生类中使用基类的同名成员,显示使用类名限定符)。任何时候重新定义基类中的一个重载函数,在新类中所有的其他版本将被自动隐藏.。
- 非自动继承的函数:构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。另外operator=也不能被继承,因为它完成类似构造函数的行为。在继承的过程中,如果没有创建这些函数,编译器会自动生成它们。
- 继承中的静态成员特性:共同点:都可以被继承到派生类中。重新定义一个静态成员函数,所有在基类中的其他重载函数会被隐藏。改变基类中一个函数的特征,所有使用该函数名的基类版本都会被隐藏。
- 多继承:指多个基类派生出一个派生类的继承关系,多继承的派生类直接继承了不止一个基类的特征。多继承会带来一些二义性的问题, 如果两个基类中有同名的函数或者变量,那么通过派生类对象去访问这个函数或变量时就不能明确到底调用从基类1继承的版本还是从基类2继承的版本。解决方法就是显示指定调用那个基类的版本。
- 菱形继承:两个派生类继承同一个基类而又有某个类同时继承者两个派生类,这种继承被称为菱形继承,或者钻石型继承。菱形继承带来两个问题:调用二义性可通过指定调用那个基类的方式来解决。重复继承同一数据,采用虚基类方式解决。
- 虚继承实现原理:普通继承和虚继承的对象内存图是不一样的。BigBase 菱形最顶层的类,内存布局图没有发生改变。Base1和Base2通过虚继承的方式派生自BigBase,这两个对象的布局图中可以看出编译器为我们的对象中增加了一个vbptr (virtual base pointer),vbptr指向了一张表,这张表保存了当前的虚指针相对于虚基类的首地址的偏移量。Derived派生于Base1和Base2,继承了两个基类的vbptr指针,并调整了vbptr与虚基类的首地址的偏移量。模型变成了Base1和 Base2 Derived三个类对象共享了一份BigBase数据。
- 当使用虚继承时,虚基类是被共享的,也就是在继承体系中无论被继承多少次,对象内存模型中均只会出现一个虚基类的子对象。即使共享虚基类,但是必须要有一个类来完成基类的初始化(因为所有的对象都必须被初始化,哪怕是默认的),同时还不能够重复进行初始化,在每一次继承子类中都必须书写初始化语句(因为每一次继承子类可能都会用来定义对象),但是虚基类的初始化是由最后的子类完成,其他的初始化语句都不会调用。注意虚继承不能解决没有公共祖先的多继承。
- 派生类继承基类,派生类拥有基类中全部成员变量和成员方法(除了构造和析构之外的成员方法),但是在派生类中,继承的成员并不一定能直接访问,不同的继承方式会导致不同的访问权限。
-
多态性。多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将”what”和”how”分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性。
- c++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。
- 向上类型转换及问题:取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。
class Animal{ public: void speak(){ cout << "动物在唱歌..." << endl; } }; class Dog : public Animal{ public: void speak(){ cout << "小狗在唱歌..." << endl; } }; void DoBussiness(Animal& animal){ animal.speak(); } void test(){ Dog dog; DoBussiness(dog); }
- 给DoBussiness传入的对象是dog,而不是animal对象,输出的结果应该是Dog::speak。把函数体与函数调用相联系称为绑定(捆绑,binding)。当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding)。C语言中只有一种函数调用方式,就是早绑定。编译器在只有Animal地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。这个时候由于DoBussiness的参数类型是Animal&,编译器确定了应该调用的speak是Animal::speak的,而不是真正传入的对象Dog::speak。
- 解决方法就是迟绑定(迟捆绑,动态绑定,运行时绑定,late binding),意味着绑定要根据对象的实际类型,发生在运行。
- C++动态多态性是通过虚函数来实现的,虚函数允许子类(派生类)重新定义父类(基类)成员函数,而子类(派生类)重新定义父类(基类)虚函数的做法称为覆盖(override),或者称为重写。对于特定的函数进行动态绑定,c++要求在基类中声明这个函数的时候使用virtual关键字,动态绑定也就对virtual函数起作用。
- 为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要。 如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual的。在派生类中virtual函数的重定义称为重写(override)。Virtual关键字只能修饰成员函数。构造函数不能为虚函数。
- C++的动态捆绑机制:当编译器发现我们的类中有虚函数的时候,编译器会创建一张虚函数表,把虚函数的函数入口地址放到虚函数表中,并且在类中秘密增加一个指针,这个指针就是vpointer(缩写vptr),这个指针是指向对象的虚函数表。在多态调用的时候,根据vptr指针,找到虚函数表来实现动态绑定。
- 在编译阶段,编译器秘密增加了一个vptr指针,但是此时vptr指针并没有初始化指向虚函数表(vtable),什么时候vptr才会指向虚函数表?在对象构建的时候,也就是在对象初始化调用构造函数的时候。编译器首先默认会在我们所编写的每一个构造函数中,增加一些vptr指针初始化的代码。如果没有提供构造函数,编译器会提供默认的构造函数,那么就会在默认构造函数里做此项工作,初始化vptr指针,使之指向本对象的虚函数表。
- 多态的成立条件:有继承。子类重写父类虚函数函数:返回值,函数名字,函数参数,必须和父类完全一致(析构函数除外)。子类中virtual关键字可写可不写,建议写。类型兼容,父类指针或引用指向子类对象。
- 抽象基类和纯虚函数:仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。要做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class)。
- 纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。
- 当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。
- Virtual void fun() = 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。
- 建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来操纵一组类,且这个公共接口不需要事先(或者不需要完全实现)。可以创建一个公共类。
- 纯虚函数和多继承:绝大数面向对象语言都不支持多继承,但是绝大数面向对象对象语言都支持接口的概念,c++中没有接口的概念,但是可以通过纯虚函数实现接口。接口类中只有函数原型定义,没有任何数据定义。多重继承接口不会带来二义性和复杂性问题。接口类只是一个功能声明,并不是功能实现,子类需要根据功能说明定义功能实现。除了析构函数外,其他声明都是纯虚函数。
- 虚析构函数:虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。纯虚析构函数在c++中是合法的,但是在使用的时候有一个额外的限制:必须为纯虚析构函数提供一个函数体。纯虚析构函数和非纯析构函数之间唯一的不同之处在于纯虚析构函数使得基类是抽象类,不能创建基类的对象。如果类的目的不是为了实现多态,作为基类来使用,就不要声明虚析构函数,反之,则应该为类声明虚析构函数。
- 重写,重载,重定义:重载,同一作用域的同名函数。同一个作用域。参数个数,参数顺序,参数类型不同。 和函数返回值,没有关系。const也可以作为重载条件。重定义(隐藏):有继承。子类(派生类)重新定义父类(基类)的同名成员(非virtual函数)。重写(覆盖):有继承。子类(派生类)重写父类(基类)的virtual函数。函数返回值,函数名字,函数参数,必须和基类中的虚函数一致。
四、C++模板
- 函数模板,实际上是建立一个通用函数,其函数类型和形参类型不具体制定,用一个虚拟的类型来代表。这个通用函数就成为函数模板。凡是函数体相同的函数都可以用这个模板代替,不必定义多个函数,只需在模板中定义一次即可。在调用函数时系统会根据实参的类型来取代模板中的虚拟类型,从而实现不同函数的功能。
- c++提供两种模板机制:函数模板和类模板。类属 :类型参数化,又称参数模板。
- 模板把函数或类要处理的数据类型参数化,表现为参数的多态性,成为类属。
- 模板用于表达逻辑结构相同,但具体数据元素类型不同的数据对象的通用行为。
- 用模板是为了实现泛型,可以减轻编程的工作量,增强函数的重用性。
- 使用函数模板实现对两个相同数据类型变量交换。
- 使用函数模板实现对char和int类型数组进行排序。
- 函数模板和普通函数区别:函数模板不允许自动类型转化,普通函数能够自动进行类型转化。
- 函数模板和普通函数在一起调用规则:c++编译器优先考虑普通函数。可以通过空模板实参列表的语法限定编译器只能通过模板匹配。函数模板可以像普通函数那样可以被重载。如果函数模板可以产生一个更好的匹配,那么选择模板。
- 模板机制剖析:编译过程:预处理(Pre-processing) -> 编译(Compiling) ->汇编(Assembling) -> 链接(Linking)。实现机制如下。
- 编译器并不是把函数模板处理成能够处理任何类型的函数。
- 函数模板通过具体类型产生不同的函数。
- 编译器会对函数模板进行两次编译,在声明的地方对模板代码本身进行编译,在调用的地方对参数替换后的代码进行编译。
- 模板的局限性:编写的模板函数很可能无法处理某些类型,另一方面,有时候通用化是有意义的,但C++语法不允许这样做。为了解决这种问题,可以提供模板的重载,为这些特定的类型提供具体化的模板,具体化优先于常规模板。
- 类模板:类模板用于实现类所需数据的类型参数化。类模板不可以使用自动类型推导,只能用显示指定类型。类模板中可以有默认参数。
- 类模板做函数参数:指定传入类型。参数模板化。整个类模板化。
- 类模板派生普通类:子类实例化的时候需要具体化的父类,子类需要知道父类的具体类型是什么样的。
- 类模板派生类模板:继承类模板的时候,必须要确定基类的大小。必须要指定出父类中的T数据类型,才能给子类分配内存。
- 类模板类内实现:类模板不能进行类型自动推导。类模板类外实现。
- 类模板头文件和源文件分离问题:类模板需要二次编译,在出现模板的地方编译一次,在调用模板的地方再次编译。C++编译规则为独立编译。解决方案: 类模板的声明和实现放到一个文件中,我们把这个文件命名为.hpp(约定)。
- 模板类碰到友元函数:友元函数在类内实现,友元函数类外实现:告诉编译器这个函数模板是存在,加上<>空参数列表,告诉编译去匹配函数模板。类模板碰到友元函数模板。
类模板的应用:数组类封装。将类写到myArray.hpp中。属性:指向堆区数组指针,数组容量,数组大小。行为:有参构造、拷贝构造、析构,operator= 、operator[] ,getCapacity 、getSize、pushback。
五、C++类型转换
- 静态转换(static_cast):用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。用于基本数据类型之间的转换,如把int转换成char,把char转换成int。这种转换的安全性也要开发人员来保证。
- 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
- 动态转换(dynamic_cast):dynamic_cast主要用于类层次间的上行转换和下行转换;dynamic_cast主要用于类层次间的上行转换和下行转换;在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全;不支持基础数据类型转换。
- 常量转换(const_cast):常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;不能直接对非指针和非引用的变量使用const_cast操作符去直接移除它的const。
- 重新解释转换(reinterpret_cast):这是最不安全的一种转换机制,最有可能出问题。主要用于将一种数据类型从一种类型转换为另一种类型。它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针。
六、C++异常
-
异常基本概念:让一个函数在发现了自己无法处理的错误时抛出(throw)一个异常,然后它的(直接或者间接)调用者能够处理这个问题。将问题检测和问题处理相分离。 异常处理就是处理程序中的错误。所谓错误是指在程序运行的过程中发生的一些异常事件(如:除0溢出,数组下标越界,所要读取的文件不存在,空指针,内存不足等等)。
-
C语言中对错误的处理总是围绕着两种方法:一是使用整型的返回值标识错误;二是使用errno宏(可以简单的理解为一个全局整型变量)去记录错误。这两种方法最大的缺陷就是会出现不一致问题。例如有些函数返回1表示成功,返回0表示出错;而有些函数返回0表示成功,返回非0表示出错。还有一个缺点就是函数的返回值只有一个,你通过函数的返回值表示错误代码,那么函数就不能返回其他的值。
-
c++异常机制相比C语言异常处理的优势:
- 函数的返回值可以忽略,但异常不可忽略。
- 整型返回值没有任何语义信息。而异常却包含语义信息,有时你从类名就能够体现出来。
- 整型返回值缺乏相关的上下文信息。异常作为一个类,可以拥有自己的成员,这些成员就可以传递足够的信息。
- 异常处理可以在调用跳级。假设在有多个函数的调用栈中出现了某个错误,使用整型返回码要求你在每一级函数中都要进行处理。而使用异常处理的栈展开机制,只需要在一处进行处理就可以了,不需要每级函数都处理。
-
异常基本语法:
- 若有异常则通过throw操作创建一个异常对象并抛出。将可能抛出异常的程序段放到try块之中。如果在try段执行期间没有引起异常,那么跟在try后面的catch字句就不会执行。catch子句会根据出现的先后顺序被检查,匹配的catch语句捕获并处理异常(或继续抛出异常)。如果匹配的处理未找到,则运行函数terminate将自动被调用,其缺省功能调用abort终止程序。处理不了的异常,可以在catch的最后一个分支,使用throw,向上抛。
- c++异常处理使得异常的引发和异常的处理不必在一个函数中,这样底层的函数可以着重解决具体问题,而不必过多的考虑异常的处理。上层调用者可以在适当的位置设计对不同类型异常的处理。
- 异常机制和函数机制互不干涉,但是捕捉方式是通过严格类型匹配。
- 异常被抛出后,从进入try块起,到异常被抛掷前,这期间在栈上构造的所有对象,都会被自动析构。析构的顺序与构造的顺序相反,这一过程称为栈的解旋(unwinding)。
- 异常接口声明:为了加强程序的可读性,可以在函数声明中列出可能抛出异常的所有类型。例如:void func() throw(A,B,C);这个函数func能够且只能抛出类型A,B,C及其子类型的异常。如果在函数声明中没有包含异常接口声明,则此函数可以抛任何类型的异常,例如:void func()。一个不抛任何类型异常的函数可声明为:void func() throw()。如果一个函数抛出了它的异常接口声明所不允许抛出的异常,unexcepted函数会被调用,该函数默认行为调用terminate函数中断程序。
- 异常变量生命周期:throw的异常是有类型的,可以是数字、字符串、类对象。throw的异常是有类型的,catch需严格匹配异常类型。
- 异常的多态使用:提供基类异常类class BaseException,子类空指针异常和 越界异常 继承 BaseException,重写virtual void printError()。
-
C++标准异常库:标准库中也提供了很多的异常类,它们是通过类继承组织起来的。引入头文件 #include <stdexcept>。
- logic_error类及其子类、runtime_error类及其子类,它们的构造函数是接受一个string类型的形式参数,用于异常信息的描述。
- 所有的异常类都有一个what()方法,返回const char* 类型(C风格字符串)的值,描述异常信息。
-
编写自己的异常类。标准库中的异常是有限的;在自己的异常类中,可以添加自己的信息。(标准库中的异常类值允许设置一个用来描述异常的字符串)。
- 建议自己的异常类要继承标准异常类。
- 当继承标准异常类时,应该重载父类的what函数和虚析构函数。
- 因为栈展开的过程中,要复制异常类型,那么要根据你在类中添加的成员考虑是否提供自己的复制构造函数。
- 将string 转为 const char * :.c_str()。const char * 可以隐式类型转换为 string ,反之不可以。
七、C++输入和输出流
- 流的概念和流类库的结构:C++输入输出包含以下三个方面的内容。
- 对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准的输入输出,简称标准I/O。
- 以外存磁盘文件为对象进行输入和输出,即从磁盘文件输入数据,数据输出到磁盘文件。以外存文件为对象的输入输出称为文件的输入输出,简称文件I/O。
- 对内存中指定的空间进行输入和输出。通常指定一个字符数组作为存储空间(实际上可以利用该空间存储任何信息)。这种输入和输出称为字符串输入输出,简称串I/O。
- C++编译系统提供了用于输入输出的iostream类库。在iostream类库中包含许多用于输入输出的类。
- 与iostream类库有关的头文件:iostream 包含了对输入输出流进行操作所需的基本信息。fstream 用于用户管理的文件的I/O操作。strstream 用于字符串流I/O。stdiostream 用于混合使用C和C + +的I/O机制时,例如想将C程序转变为C++程序。iomanip 在使用格式化I/O时应包含此头文件。
- 在iostream头文件中不仅定义了有关的类,还定义了4种流对象:cin 标准输入流,cout 标准输出流,cerr 标准错误流,clog 标准错误流。
- cout不是C++预定义的关键字,它是ostream流类的对象,cout流是流向显示器的数据。cout流中的数据是用流插入 运算符“<<”顺序加入的。
*用“cout<<”输出基本类型的数据时,可以不必考虑数据是什么类型,系统会判断数 据的类型,并根据其类型选择调用与之匹配的运算符重 载函数。这个过程都是自动的, 用户不必干预。 - cout流在内存中对应开辟了一个缓冲区,用来存放流中的数据,当向cout流插人一个endl时,不论缓冲区是否已满,都立即输出流中所有数据,然后插入一个换行符, 并刷新流(清空缓冲区)。注意如果插人一个换行符”\n“(如cout<<a<<“\n”),则只输出和换行,而不刷新cout 流(但并不是所有编译系统都体现出这一区别)。
- 在iostream中只对"<<“和”>>“运算符用于标准类型数据的输入输出进行了重载,但未对用户声明的类型数据的输入输出进行重载。用户声明了新的类型,需要重载”<<“和”>>"。
- cout不是C++预定义的关键字,它是ostream流类的对象,cout流是流向显示器的数据。cout流中的数据是用流插入 运算符“<<”顺序加入的。
- cerr流对象是标准错误流,cerr流已被指定为与显示器关联。cerr的 作用是向标准错误设备(standard error device)输出有关出错信息。cerr流中的信息只能在显示器输出。cerr流中的信息是用户根据需要指定的。
- clog流对象也是标准错误流,它是console log的缩写。它的作用和cerr相同,都是在终端显示器上显示出错信息。区别:cerr是不经过缓冲区,直接向显示器上输出有关信息,而clog中的信息存放在缓冲区中,缓冲区满后或遇endl时向显示器输出。
-
标准输入流:标准输入流对象cin。
- cin.get() //一次只能读取一个字符
- //链式编程char char1, char2, char3, char4; cin.get(char1).get(char2).get(char3).get(char4);
- cin.get(两个参数) //可以读字符串,换行符遗留在缓冲区中。
- cin.getline()获取字符串,换行符直接扔掉。
- cin.ignore()//忽略缓冲区当前字符,默认忽略1个字符。
- cin.peek()//偷窥缓冲区的数据
- cin.putback()//将数据放回缓冲区原位置。
- 案例1:判断用户输入的内容是字符串还是数字。
- 案例2:用户输入 0 ~ 10 之间的数字,如果输入有误,重新输入。
- 标志位:cin.fail() 0代表正常 1代表异常。
- cin.clear() cin.sync() 清空缓冲区 重置标志位。
- cin.get() //一次只能读取一个字符
-
标准输出流:
- 字符输出:cout.flush() //刷新缓冲区 Linux下有效;cout.put() //向缓冲区写字符;cout.write() //从buffer中写num个字节到当前输出流中。
- 格式化输出:使用流对象的有关成员函数和控制符格式化输出。
-
使用流对象的有关成员函数:
cout.width(20);//指定宽度为20 cout.fill('*'); //填充 cout.setf(ios::left);//左对齐 cout.unsetf(ios::dec); //卸载十进制 cout.setf(ios::hex);//安装十六进制 cout.setf(ios::showbase);//显示基数 cout.unsetf(ios::hex);//卸载十六进制 cout.setf(ios::oct);//安装八进制
-
控制符格式化输出: 引入头文件 #include< iomanip>
int number = 99; cout << setw(20) << setfill('~') << setiosflags(ios::showbase) << setiosflags(ios::left) << hex << number << endl;
-
- 成员函数width(n)和控制符setw(n)只对其后的第一个输出项有效。
- 表13.5中的输出格式状态分为5组,每一组中同时只能选用一种,它们是互相排斥的。在用成员函数setf和 控制符setiosflags设置输出格式状态后,如果想改设置为同组的另一状态,应当调用成员函数unsetf(对应于成员函数self)或 resetiosflags(对应于控制符setiosflags),先终止原来设置的状态。然后再设置其他状态
- 用setf 函数设置格式状态时,可以包含两个或多个格式标志,由于这些格式标志在ios类中被定义为枚举值,每一个格式标志以一个二进位代表,因此可以用位或运算符“|”组合多个格式标志。
-
文件读写:输入输出是以系统指定的标准设备(输入设备为键盘,输出设备为显示器)为对象的。在实际应用中,常以磁盘文件作为对象。即从磁盘文件读取数据,将数据输出到磁盘文件。
- 和文件有关系的输入输出类主要在fstream.h这个头文件中被定义,在这个头文件中主要被定义了三个类,由这三个类控制对文件的各种输入输出操作,他们分别是ifstream、ofstream、fstream,其中fstream类是由iostream类派生而来。ifstream类,它是从istream类派生的,用来支持从磁盘文件的输入。ofstream类,它是从ostream类派生的,用来支持向磁盘文件的输出。fstream类,它是从iostream类派生的,用来支持对磁盘文件的输入输出。
- 打开文件:调用文件流的成员函数open。
ofstream outfile; //定义ofstream类(输出文件流类)对象outfile outfile.open("f1.dat",ios::out); //使文件流与f1.dat文件建立关联
- 第2行是调用输出文件流的成员函数open打开磁盘文件f1.dat,并指定它为输出文件, 此时f1.dat是一个输出文件,接收从内存 输出的数据。磁盘文件名可以包括路径,如"c:\new\f1.dat",如缺省路径,则默认为当前目录下的文件。
- 在定义文件流对象时指定参数:可以在定义文件流对象时指定参数,调用文件流类的构造函数来实现打开文件的功能。
- 每一个打开的文件都有一个文件指针,该指针的初始位置由I/O方式指定,每次读写都从文件指针的当前位置开始。每读入一个字节,指针就后移一个字节。当文件指针移到最后,就会遇到文件结束EOF(文件结束符也占一个字节,其值为-1),此时流对象的成员函数eof的值为非0值(一般设为1),表示文件结束了。
- 可以用“位或”运算符“|”对输入输出方式进行组合。
- 如果打开操作失败,open函数的返回值为0(假),如果是用调用构造函数的方式打开文件的,则流对象的值为0。
- C++关闭文件:在对已打开的磁盘文件的读写操作完成后,应关闭该文件。关闭文件用成员函数close。如:outfile.close( ); //将输出文件流所关联的磁盘文件关闭。
- C++对ASCII文件的读写操作:如果文件的每一个字节中均以ASCII代码形式存放数据,即一个字节存放一个字符,这个文件就是ASCII文件(或称字符文件)。程序可以从ASCII文件中读入若干个字符,也可以向它输出一些字符。
- “<<”和“ >>”都巳在iostream中被重载为能用于ostream和istream类对象的标准类型的输入输出。所以在对磁盘文件的操作中,可以通过文件流对象和流插入运算符“<<”及 流提取运算符“>>”实现对磁盘文件的读写。
- 用文件流的put、get、geiline等成员函数进行字符的输入输出,:用C++流成员函数put输出单个字符、C++ get()函数读入一个字符和C++ getline()函数读入一行字符。
- C++对二进制文件的读写操作:对二进制文件的操作也需要先打开文件,用完后要关闭文件。在打开时要用ios::binary指定为以二进制形式传送和存储。二进制文件除了可以作为输入文件或输出文件外,还可以是既能输入又能输出的文件。这是和ASCII文件不同的地方。
- 用成员函数read和write读写二进制文件。
istream& read(char *buffer,int len); ostream& write(const char * buffer,int len); //字符指针buffer指向内存中一段存储空间。len是读写的字节数。
- 用成员函数read和write读写二进制文件。
总结
本文章涉及思维导图及代码:https://github.com/wangchengyongnevergiveup/C-Programming.git
笔记来源:黑马程序员C语言课程