C++课程笔记

版权声明:欢迎转载 https://blog.csdn.net/suntengnb/article/details/82816997

C++基础

开始

在大多数系统中,main的返回值被用来指示状态。返回值0表示成功,非0的返回值的含义由系统定义,通常用来指出错误类型。

访问main的返回值的方法依赖于系统。在UNIX和Windows系统中,执行完一个程序后,都可以通过echo命令获得其返回值。在UNIX系统中,通过如下的命令获得状态:$ echo $?。在Windows系统中查看状态可键入:$ echo %ERRORLEVEL%

最常用的编译器是GNU编译器和微软Visual Studio编译器。默认情况下,运行GNU编译器的命令是g++:$ g++ -0 prog1 prog1.cc,此处,$是系统提示符。-o prog1是编译参数,指定了可执行文件的文件名

流就是字符序列

缓冲刷新操作可以保证目前为止程序所产生的所有输出都真正写入输出流中,而不是仅停留在内存中等待写入流

程序员常常在调试时添加打印语句。这类语句应该保证“一直”刷新流。否则如果程序崩溃,输出还可能留在缓冲区中,从而导致关于程序崩溃位置的错误推断

命名空间可以帮助我们避免不经意的名字定义冲突,以及使用库中相同名字导致的冲突。标准库定义的所有名字都在命名空间std

注释界定符/*...*/不能嵌套

我们也需要使用头文件来访问为自己的应用程序所定义的类。习惯上,头文件根据其中定义的类的名字来命名

包含来自标准库的头文件时,也应该用尖括号(<>)包围头文件名。对于不属于标准库的头文件,则用双引号("")包围。

():调用运算符

全局作用域(::)是最大的作用域

软件 = 程序 + 文档,其中程序 = 算法 + 数据结构

类 = 接口 + 实现

一个合格的软件应该做到无论是合法的输入还是非法的输入都能接受

计算的本质就是模拟

学习程序设计思维方式的秘诀是:读-抄-运行-改-运行-扩充-运行-独立实现-运行

不需要在掌握了一个新语言的所有语言特性和技术内涵后才开始真正使用它,我们可以在实践过程中循序渐进地逐渐掌握它

C++支持的程序设计泛型有:过程、模块化、函数、逻辑、面向对象和泛型

成员函数:应做到先检查有效性,再修改数据成员

变量和基本类型

大多数计算机以2的整数幂次个比特作为块来处理内存,可寻址的最小内存块称为“字节(byte)”,存储的基本单元称为“字(Word)”。大多数机器的字节由8比特构成,字则由32或64比特构成,也就是4或8字节

类型char在一些机器上是有符号的,而在另一些机器上又是无符号的

执行浮点数运算选用double

当我们赋给无符号类型一个超出它表示范围的值时,结果是初始值对无符号类型表示数值总数取模后的余数

当我们赋给带符号类型一个超出它表示范围的值时,结果是未定义的。此时,程序可能继续工作、可能崩溃,也可能生成垃圾数据

建议:避免无法预知和依赖于实现环境的行为

换行符\n,回车符/r

\x后可跟1个或多个十六进制数字,\后可跟1个、2个或3个八进制数字

声明使得名字为程序所知,一个文件如果想使用别处定义的名字则必须包含对那个名字的声明

定义负责创建与名字关联的实体

如果想声明一个变量而非定义它,就在变量名前添加关键字extern,而且不要显示初始化变量

任何包含了显式初始化的声明即成为定义

变量只能被定义一次,但是可以多次声明

用户自定义的类名一般以大写字母开头

引用必须被初始化

引用本身不是一个对象,没有实际地址,故不能定义指向引用的指针

预处理变量不属于命名空间std,它由预处理器负责管理,因此我们可以直接使用预处理变量而无需在前面加上std::

要想理解变量名的类型,从右向左阅读变量的定义,离变量名最近的符号对变量名有最直接的影响

常量对象必须被初始化

如果想在多个文件之间共享const对象,必须在变量的定义之前添加extern关键字

和常量引用一样,指向常量的指针也没有规定其所指的对象必须是一个常量。所谓指向常量的指针或引用,不过是指针或引用“自以为是”罢了,它们觉得自己指向了常量,所以自觉地不去改变所指对象的值

指针本身是一个常量并不意味着不能通过指针修改其所指对象的值,能否这样做完全依赖于所指对象的类型

用名词**顶层const(top-level const)表示指针本身是一个常量,而用名词底层const(low-level const)**表示指针所指的对象是一个常量

一般来说,非常量可以转换成常量,反正不行

非常量引用或指针不能指向常量对象

C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量表达式。声明为constexpr的变量一定是一个常量,而且必须用常量表达式初始化

auto让编译器通过初始值来推算变量的类型。显然,auto定义的变量必须有初始值

为了确保各个文件中类的定义一致,类通常被定义在头文件中,而且类所在头文件的名字应该与类的名字一样

头文件通常包含那些只能被定义一次的实体,如类、const和constexpr变量等

防止头文件被多次包含:头文件保护符

预处理变量无视C++语音中关于作用域的规则

头文件中不应包含using声明

类型包括值集和操作集

字符字面值、整数字面值

十六进制\xhh或0x~八进制:\ddd或0~

enumerations(枚举)是用户采用枚举值集的方式定义的类型,值集中的每个值都是一个已命名的整数常量,该名字就是该枚举类型的一个字面值。枚举类型可进行赋值、算数和关系运算

C++中,声明分为定义声明和非定义声明。

  • 对于变量(对象)声明,若没有描述符extern,或它带有初始式,则为定义声明,否则仅为非定义声明
  • 对于函数声明,若带有函数体,则为定义声明,否则仅为非定义声明
  • 一个enum、struct、union、class,若带有其所有成员的声明,则为定义声明;否则仅为非定义声明。
  • 名字空间(namespace)只有定义声明,而没有非定义声明的形式

绑定:由编译程序将一个名字与它对应的存储空间关联在一起的动作

字符串、向量和数组

使用getline读取一整行

int main()
{
    string line;
    while(getline(cin,line))
        cout << line << endl;
    return 0;
}

因为某些历史原因,也为了与C兼容,所以C++语言中的字符串字面值并不是标准库类型string的对象

在名为cname的头文件定义中定义的名字从属于命名空间std,而定义在名为.h的头文件中的则不然

有效的迭代器或者指向某个元素,或者指向容器中尾元素的下一位置,其他所有情况都属于无效

如果容器为空,则beginend返回的是同一个迭代器

但凡是使用了迭代器的循环体,都不要向迭代器所属的容器添加元素

内置的下标运算符所用的索引值不是无符号类型,这一点与vector和string不一样

string s = "Hello World";
char* str = s.c_str();

要使用范围for语句处理多维数组,除了最内层的循环外,其他所有循环的控制变量都应该是引用类型

指针常量:定义方式为T* const ID = 初值;,不允许修改的是ID的值,而不是“*ID”及“ID[下标]”的值。引用类似于指针常量

结构体类型变量的存储分配一般遵循字对齐原则,因此这块空间可能大于各成员所占空间之和

基本类型间的转换规则?

表达式

当一个对象被用作右值的时候,用得是对象的值(内容),当对象被用作左值的时候,用的是对象的身份(在内存中的位置)

一个重要的原则是在需要右值的时候可以用左值来代替,但是不能把右值当成左值(也就是位置)使用

算术运算符、关系运算符和逻辑运算符的运算对象和求值结果都是右值

C++11新标准规定商一律向0取整(即直接切除小数部分)

根据取余运算的定义,如果m和n是整数且n非0,则表达式(m / n) * n + m % n的求值结果和m相等。隐含的意思是,如果m % n不等于0,则它的符号和m相同

声明成引用类型可以避免对元素的复制

对于int val = 2; if(val == true){},如果val不是布尔值,那么进行比较之前会首先把true转换成val的类型。也就是说,如果val不是布尔值,则代码可以改写成如下的形式if(val == 1){}。所以,当进行比较运算时,除非比较的对象是布尔类型,否则不要使用布尔字面值true和false作为运算对象

赋值运算符的左侧运算对象必须是一个可修改的左值

自增自减运算符的前置版本将对象本身作为左值返回,后置版本则将对象原始值的副本作为右值返回

建议:除非必须,否则不用递增递减运算符的后置版本

箭头运算符作用于一个指针类型的运算对象,结果是一个左值。点运算符分成两种情况:如果成员所属的对象是左值,那么结果是左值;反之如果成员所属的对象是右值,那么结果是右值

当条件运算符的两个表达式都是左值或者能转换成同一种左值类型时,运算的结果是左值;否则运算的结果是右值。

运算对象可以是带符号的,也可以是无符号的。如果运算对象是带符号的且它的值为负,那么位运算符如何处理运算对象的“符号位”依赖于机器。而且,此时的左移操作可能会改变符号位的值,因此是一种未定义行为

关于符号位如何处理没有明确的规定,所以强烈建议仅将位运算符用于处理无符号类型

重载运算符的优先级和结合律都与它的内置版本一样

逗号运算符含有两个运算对象,按照从左向右的顺序依次求值

对于逗号运算符来说,首先对左侧的表达式求值,然后将求值结果丢弃掉。逗号运算符真正的结果是右侧表达式的值

C++语言不会将两个不同类型的值相加,而是先根据类型转换规则设法将运算对象的类型统一后再求值

如果一个运算对象是无符号类型、另外一个运算对象是带符号类型,而且其中的无符号类型不小于带符号类型,那么带符号的运算对象转换成无符号的

多维数组声明时,第一维的元素个数可不给出,但其他维的元素个数必须给出

若二元操作符的两个操作数类型不同,其结果的类型(也就是这个表达式的类型)为值集(存储空间)较大者的类型

前缀一元运算、赋值运算是右结合,其他均为左结合

语句

表达式语句的作用是执行表达式并丢弃掉求值结果

就C++而言,它规定else与离它最近的尚未匹配的if匹配,从而消除了程序的二义性

抛出异常将终止当前的函数,并把控制权转移给能处理该异常的代码

主调函数、被调函数

函数

如果一个函数永远也不会被我们用到,那么它可以只有声明没有定义

建议变量在头文件中声明,在源文件中定义。与之类似,函数也应该在头文件中声明而在源文件中定义

如果函数无须改变引用形参的值,最好将其声明为常量引用

当使用argv中的实参时,一定要记得可选的实参从argv[1]开始;argv[0]保存程序的名字,而非用户输入

调用一个返回引用的函数得到左值,其他返回类型得到右值。可以像使用其他左值那样来使用返回引用的函数的调用,特别是,我们能为返回类型是非常量引用的函数的结果赋值

当设计含有默认实参的函数时,其中一项任务便是合理设置形参的顺序,尽量让不怎么使用默认值的形参出现在前面,而让那些经常使用默认值的形参出现在后面

函数匹配:第一步,选定本次调用对应的重载函数集;第二步,考察本次调用提供的实参,然后从候选参数中选出能被这组实参调用的函数

为保证函数的定义、声明一致,通常将函数声明在头文件中

何时传值,何时传引用?以下情况下,传引用,否则传值

  • 希望将函数对形参的修改结果带回给调用者(合理利用传引用)
  • 实参的size较大、结构较复杂,传引用可避免实参被复制

在返回值类型为void的函数体中,可以return另一个返回类型为void的函数调用

仅仅是返回值类型不同形参名不同的两个同名函数,不是重载函数,将被认为是对同一个函数的重复定义

仅当参数的个数和类型都不确定时,才有必要使用省略号。否则,应当尽可能使用重载函数或声明为有缺省参数的函数。

函数指针的类型根据其指向的函数类型确定,包括

  • 函数返回值类型
  • 形参表类型(参数的个数、每个参数的类型)

参数传递与返回值的语义,都与变量/常量的初始化一致

函数重置的解析规则?

命名空间

命名空间是一种表达逻辑分组关系的机制。也就是说,按照某种准则,如果一些声明在逻辑上属于一个集合,就可以把它们放入一个共同的namespace

特点:

  • 一个namespace就是一个作用域
  • namespace定义可嵌套
  • 同一namespace的定义可出现在多处,编译器会自动合并其成员,如标准库的所有名字分布在多个头文件中,但它们都是std的成员

using声明(using std::cin;)、using指令(using namespace std;)

类的基本思想是数据抽象封装

类的用户是程序员,而非应用程序的最终使用者

成员函数的声明必须在类的内部,它的定义则既可以在类的内部也可以在类的外部。作为接口组成部分的非成员函数,例如add、read和print等,它们的定义和声明都在类的外部

定义在类内部的函数是隐式的inline函数

成员函数通过一个名为this的额外的隐式参数来访问调用它的那个对象。当我们调用一个成员函数时,用请求该函数的对象地址初始化this

在成员函数内部,我们可以直接使用调用该函数的对象的成员,而无需通过成员访问符来做到这一点,因为this所指的正是这个对象。任何对类成员的直接访问都被看做this的隐式引用

const成员函数

默认情况下,this的类型是指向类类型非常量版本的常量指针

尽管this是隐式的,但它仍然需要遵循初始化规则,意味着(在默认情况下)我们不能把this对象绑定到一个常量对象上

紧跟在参数列表后面的const表示this是一个指向常量的指针。像这样使用const的成员函数被称作常量成员函数。因为this是指向常量的指针,所以常量成员函数不能改变调用它的对象的内容

常量对象,以及常量对象的引用或指针都不能改变调用它的对象的内容

编译器分两步处理类:首先编译成员的声明,然后才轮到成员函数体(如果有的话)

类外部定义的成员的名字必须包含它所属的类名(类名::)

一般来说,当我们定义的函数类似于某个内置运算符时,应该令该函数的行为尽量模仿这个运算符。内置的赋值运算符把它的左侧运算对象当成左值返回

一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内

IO类属于不能被拷贝的类型,因此只能通过引用来传递它们

默认情况下,拷贝类的对象其实拷贝的是对象的数据成员(浅拷贝)

类通过一个或几个特殊的成员函数来控制其对象的初始化过程,这些函数叫作构造函数。无论何时只要类的对象被创建,就会执行构造函数

对于大多数类来说,合成的默认构造函数将按照如下规则初始化类的数据成员:如果存在类内的初始值,用它来初始化成员,否则,默认初始化该成员

对于一个普通的类来说,必须定义它自己的默认构造函数,原因有三:第一个原因也是最容易理解的一个原因就是编译器只有在发现类不包含任何构造函数的情况下才会替我们生成一个默认的构造函数。一旦我们定义了一些其他的构造函数,那么除非我们再定义一个默认的构造函数,否则类将没有默认构造函数。第二个原因是对于某些类来说,合成的默认构造函数可能执行错误的操作。如果类包含有内置类型或复合类型的成员,则只有当这些成员全部被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数。第三个原因是有的时候编译器不能为某些类合成默认的构造函数

无任何的构造函数、默认构造函数、默认构造函数+其他构造函数。不允许只出现其他构造函数

默认构造函数是指不接受任何参数的构造函数

在C++11新标准中,如果我们需要默认的行为,那么可以通过在参数列表后面写上= default来要求编译器生成构造函数。其中,= default既可以和声明一起出现在类的内部,也可以作为定义出现在类的外部。和其他函数一样,如果= default在类的内部,则默认构造函数是内联的;如果它在类的外部,则该成员默认情况下是不内联的

当某个数据成员被构造函数初始值列表忽略时,它将以与合成默认构造函数相同的方式隐式初始化。对“忽略”的进一步解释,构造函数初始值列表是始终存在的,尽管有时它为空

初始化(构造)、拷贝、赋值、销毁(析构)。一般来说,以上操作需主动定义。管理动态内存的类通常不能依赖于上述操作的合成版本

如果类包含vector或者string成员,则其拷贝、赋值和销毁的合成版本能够正常工作

在C++语言中,我们使用访问说明符加强类的封装性。定义在public说明符之后的成员在整个程序内可被访问,public成员定义类的接口,定义在private说明符之后的成员可以被类的成员函数访问,但是不能被使用该类的代码访问,private部分封装了(即隐藏了)类的实现细节

如果我们使用struct关键字,则定义在第一个访问说明符之前的成员是public的;相反,如果我们使用class关键字,则这些成员是private的

类可以允许其他或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。非成员函数的类接口也可以访问private数据成员了

友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。一般来说,最好在类定义开始或结束前的位置集中声明友元

友元的声明仅仅指定了访问的权限,而非一个通常意义上的函数声明。如果我们希望类的用户能够调用某个友元函数,那么我们就必须在友元声明之外再专门对函数进行一次声明

为了使友元对类的用户可见,我们通常把友元的声明与类本身放置在同一个头文件中(类的外部)

和非成员函数一样,成员函数也能重载

一个可变数据成员(用关键字mutable声明)永远不会是const,即使它是const对象的成员。因此,一个const成员函数可以改变一个可变成员的值

返回*this(对象的引用)的成员函数,可以这样使用:对象.fun1().fun2()

一个const成员函数如果以引用的形式返回*this,那么它的返回类型将是常量引用

通过区分成员函数是否是const的,我们可以对其进行重载

因为非常量版本的函数对于常量对象是不可用的,所以我们只能在一个常量对象上调用const成员函数。另一方面,虽然可以在非常量对象上调用常量版本或非常量版本,但显然此时非常量版本是一个更好的匹配

不完全类型:声明之后、定义之前

不完全类型只能在非常有限的情境下使用:可以定义指向这种类型的指针或引用,也可以声明(但是不能定义)以不完全类型作为参数或者返回值类型的函数

一旦一个类的名字出现后,它就被认为是声明过了(但尚未定义),因此类允许包含指向它自身类型的引用或指针

类还可以把其他类定义成友元,也可以把其他类(之前已定义过的)的成员函数定义成友元

友元关系不存在传递性

当把一个成员函数声明成友元时,我们必须明确指出该成员函数属于哪个类

尽管重载函数的名字相同,但它们仍然是不同的函数。因此,如果一个类想把一组重载函数声明成它的友元,它需要对这组函数中的每一个分别声明

一个类名就是一个作用域,在类的外部,成员的名字被隐藏起来了

函数的返回类型通常出现在函数名之前。因此当成员函数定义在类的外部时,返回类型中使用的名字都位于类的作用域之外

编译器处理完类中的全部声明后才会处理成员函数的定义

如果成员是const、引用,或者属于某种未提供默认构造函数的类类型,我们必须通过构造函数初始值列表为这些成员提供初值

在很多类中,初始化和赋值的区别事关底层效率的问题:前者直接初始化数据成员,后者则先初始化再赋值

如果一个构造函数为所有参数都提供了默认实参,则它实际上也定义了默认构造函数

如果构造函数只接受一个实参,则它实际上定义了转换为此类类型的隐式转换机制,有时我们把这种构造函数称作转换构造函数

只允许一步类类型转换

在要求隐式转换的程序上下文中,我们可以通过将构造函数声明为explicit加以阻止。只能在类内声明构造函数时使用explicit关键字,在类外部定义时不应重复

类型 变量名(值);是直接初始化,类名 变量名 = 值;是拷贝形式的初始化

聚合类使得用户可以直接访问其成员,并且具有特殊的初始化语法形式。当一个类满足如下条件时,我们说它是聚合的:所有成员都是public的、没有定义任何构造函数、没有类内初始值、没有基类,也没有virtual函数

通过在成员的声明之前加上关键字static使得其与类关联在一起

可以使用作用于运算符直接访问静态成员

虽然静态成员不属于类的对象,但是我们仍然可以使用类的对象、引用或者指针来访问静态成员

当在类的外部定义静态成员时,不能重复static关键字,此关键字只出现在类内部的声明语句

必须在类的外部定义和初始化每个静态成员类型名 类名::静态成员名 = 初始值;

类的静态成员不属于任何对象

全部public成员函数的声明组成类的接口

静态数据成员的特点:

  • 类的所有实例共享该成员
  • 可以不通过类的任何实例访问
  • 须在类之外(全局作用域)给出定义

访问静态数据成员的方式:

  • 通过对象访问
  • 通过类型名访问,如Date::n_Date
  • 在类的方法中直接访问

静态成员函数只允许访问类自身的静态数据成员和静态函数成员

常量对象只能调用常量成员函数

this是一个常量,只能在非静态成员函数中使用

当二元操作符以类A的成员函数重载时,A的对象为第一操作数(*this),形参为第二操作数(类型任意)

当二元操作符以非成员函数重载时,第一个形参为第一操作数,第二个形参为第二操作数,这两个操作数中至少一个为自定义类型

对于前缀一元操作符@,@a可解释为:a.operator@()operator@(a)

对于后缀一元操作符@,aa@可解释为:a.operator@(int)operator@(a,int)

对于=、[]、()、->这四个操作符,必须采用非静态成员函数进行重载,以保证第一操作数一定是左值

应该当某个操作符的第一操作数是类A的对象,并且修改该对象时,才用A的成员函数来重载该操作符

为实现(用户自定义类型->基本类型),可重载“(类型)转换操作符”,例如:

operator double() const
{
    return r;
}

其中:

  • 不能指定返回值类型
  • 必须是成员函数
  • 没有参数
  • 通常可声明为const成员函数
  • 可被隐式调用

一个友元可以是一个全局函数,也可以是另外一个类的成员函数,也可以是另外一个类

=、[]、()、->,以及需要继承的操作符只能定义为成员函数而不能定义为友元

非静态常量数据成员只能通过初始化列表初始化

对于以下各类数据成员,必须在初始化列表中定义相应的初始化操作:

  • const成员
  • reference成员
  • 没有相应默认构造函数的成员

静态数据成员的类型可以是该成员所属的类类型,非静态成员被限定声明为其自身类对象的指针或引用(否则引发无限递归调用构造函数)

面向对象程序设计

面向对象程序设计基于三个基本概念:数据抽象、继承和动态绑定

通过使用数据抽象,我们可以将类的接口与实现分离;使用继承,可以定义相似的类型并对其相似关系建模;使用动态绑定,可以在一定程度上忽略相似类型的区别,而以统一的方式使用它们的对象

在C++语言中,基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数,基类希望它的派生类各自定义适合自身的版本,此时基类就将这些函数声明成虚函数

派生类必须在其内部对所有重新定义的虚函数进行声明

在C++语言中,当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定

基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此

任何构造函数之外的非静态函数都可以是虚函数

关键字virtual只能出现在类内部的声明语句之前而不能用于类外部的函数定义

成员函数如果没被声明为虚函数,则其解析过程发生在编译时而非运行时

派生类可以继承定义在基类中的成员,但是派生类的成员函数不一定有权访问从基类继承而来的成员

基类希望它的派生类有权访问该成员,同时禁止其他用户访问。我们用受保护的(protected)访问运算符说明这样的成员

如果派生类没有覆盖其基类中的某个虚函数,则该虚函数的行为类似于其他的普通成员,派生类会直接继承其在基类中的版本

在一个对象中,继承自基类的部分和派生类自定义的部分不一定是连续存储的

因为在派生类对象中含有与其基类对应的组成部分,所以我们能把派生类的对象当成基类对象来使用,而且我们也能将基类的指针或引用绑定到派生类对象中的基类部分上。这种转换通常称为派生类到基类的类型转换

派生类也必须使用基类的构造函数来初始化它的基类部分

首先初始化基类的部分,然后按照声明的顺序依次初始化派生类的成员

如果基类定义了一个静态成员,则在整个继承体系中只存在该成员的唯一定义。不论从基类中派生出来多少个派生类,对于每个静态成员来说都只存在唯一的实例

派生类的声明和其他类别相差不大,声明中包含类名但是不包含它的派生列表(什么是派生列表?)

如果我们想将某个类用作基类,则该类必须已经定义而非仅仅声明

如果表达式既不是引用也不是指针,则它的动态类型永远与静态类型一致

一个基类的对象既可以以独立的形式存在,也可以作为派生类对象的一部分存在

当我们用一个派生类对象为一个基类对象初始化或赋值时,只有该派生类对象中的基类部分会被拷贝、移动或赋值,它的派生类部分将被忽略掉

含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类

派生类构造函数只初始化它的直接基类

protected的重要性质:派生类的成员或友元只能通过派生类对象来访问基类的受保护成员。派生类对于一个基类对象中的受保护成员没有任何访问特权

某个类对其继承而来的成员的访问权限受到两个因素影响:一是在基类中该成员的访问说明符,二是在派生类的派生列表中的访问说明符

派生访问说明符对于派生类的成员(或友元)能否访问其直接基类的成员没什么影响。对基类成员的访问权限只与基类中的访问说明符有关

派生访问说明符的目的是控制派生类用户(包括派生类的派生类在内)对于基类成员的访问权限

假定D继承自B:

  • 只有当D公有地继承B时,用户代码才能使用派生类向基类的转换;如果D继承B的方式是受保护的或者私有的,则用户代码不能使用该转换
  • 不论D以什么方式继承B,D的成员函数和友元都能使用派生类向基类的转换;派生类向其直接基类的类型转换对于派生类的成员和友元永远是可访问的
  • 如果D继承B的方式是公有的或受保护的,则D的派生类的成员和友元可以使用D向B的类型转换;反之,如果D继承B的方式是私有的,则不能使用

public继承:不改变基类成员的能见度,如B的public成员可被任何函数访问。B的protected成员只可被D、D的友元、D的派生类及其友元访问

protected继承:将B的public成员能见度降低为protected,其余不变。如B的public、protected成员只可被D、D的友元、D的派生类及友元访问

private继承:B的所有成员能见度降低为private。如B的public、protected成员只能被D、D的友元访问

不考虑继承的话,我们可以认为一个类有两种不同的用户:普通用户和类的实现者。其中,普通用户编写的代码使用类的对象,这部分代码只能访问类的公有(接口)成员;实现者则负责编写类的成员和友元的代码,成员和友元既能访问类的公有部分,也能访问类的私有(实现)部分

就像友元关系不能传递一样,友元关系同样也不能继承

对基类的访问权限由基类本身控制,即使对于派生类的基类部分也是如此

默认情况下,使用class关键字定义的派生类是私有继承的;而使用struct关键字定义的派生类是公有继承的

当存在继承关系时,派生类的作用域嵌套在基类的作用域之内。如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义

一个对象、引用或指针的静态类型决定了该对象的哪些成员是可见的。即使静态类型与动态类型可能不一致(当使用基类的引用或指针时会发生这种情况),但是我们能使用哪些成员仍然是由静态类型决定的

如果派生类(即内层作用域)的成员与基类(即外层作用域)的某个成员同名,则派生类将在其作用域内隐藏该基类成员。即使派生类成员和基类成员的形参列表不一致,基类成员也仍然会被隐藏掉。也就是:名字查找先于类型检查

has a表示整体和部分的关系,is a表示一般和特殊的关系

当通过基类的指针或引用访问派生类的对象时,它可被当作基类的对象看待

派生类对象构造时,自动调用基类的constructor,派生类对象销毁时,自动调用基类的destructor

对于基类的任一虚函数,被派生类继承后,在派生类中仍为虚函数(无论派生类是否重置,也就是说,允许隔代重置)

若虚函数定义在类外,则关键字virtual不能出现

应注意,若基类存在纯虚函数,且派生类没有重置,则:

  • 该函数在派生类中仍然是虚函数
  • 派生类也是抽象类

多个直接基类的继承顺序:

  • 继承方式不同时,虚继承优先
  • 继承方式相同时,按继承声明顺序

类成员的访问控制方式(即能见度):

  • public,一般用户、派生类成员函数和友元、自身成员函数和友元
  • protected,派生类成员函数和友元、自身成员函数和友元
  • private,自身成员函数和友元

组合复用原则:要尽量使用组合,尽量不要使用继承

模板

实现一个模板的一般过程:

  • 先定义一个特定类型的实现,并调试好
  • 将该实现通用化

异常

异常总是用对象来表示,它包含了错误的相关信息:

  • 错误类型:用对象的类型来表示
  • 错误原因:用对象的属性来表示
  • 其他与错误相关的信息:用对象的属性来表示
try
{
    throw;
}
catch()
{
    
}

猜你喜欢

转载自blog.csdn.net/suntengnb/article/details/82816997
今日推荐