1.参数的传递本质上是一次赋值的过程,赋值就是对内存进行拷贝。所谓内存拷贝,是指将一块内存上的数据复制到另一块内存上。
对于像 char、bool、int、float等基本类型的数据,它们占用的内存往往只有几个字节,对它们进行内存拷贝非常快速。而数组、结构体、对象是一系列数据的集合,数据的数量没有限制,可能很少,也可能成千上万,对它们进行频繁的内存拷贝可能会消耗很多时间,拖慢程序的执行效率。C/C++禁止在函数调用时直接传递数组的内容,而是强制传递数组指针,而对于结构体和对象没有这种限制,调用函数时既可以传递指针,也可以直接传递内容;
但是在 C++ 中,我们有了一种比指针更加便捷的传递聚合类型数据的方式,那就是引用(Reference)。
2. 引用(Reference)是C++ 相对于C语言的又一个扩充。引用可以看做是数据的一个别名,通过这个别名和原来的名字都能够找到这份数据。引用类似于Windows 中的快捷方式,一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序;引用还类似于人的绰号(笔名),使用绰号(笔名)和本名都能表示一个人。
引用的定义方式类似于指针,只是用& 取代了* ,语法格式为:
type &name = data;
type 是被引用的数据的类型
name 是引用的名称
data 是被引用的数据。
引用必须在定义的同时初始化,并且以后也要从一而终,不能再引用其它数据,这有点类似于常量(const变量)。
3. 本例中,变量 b 就是变量 a 的引用,它们用来指代同一份数据;也可以说变量b 是变量a 的另一个名字。从输出结果可以看出,a和 b 的地址一样,或者说这块地址的内存有两个名字,a和 b,想要访问该内存上的数据时,使用哪个名字都行。
注意,引用在定义时需要添加&,在使用时不能添加& ,使用时添加& 表示取地址。如上面代码所示,的&表示引用,第 8行中的&表示取地址。除了这两种用法, &还可以表示位运算中的与运算。
4. 如果不希望通过引用来修改原始的数据,那么可以在定义时添加const限制,形式为:
const type &name = value;
也可以是:
type const &name = value;
这种引用方式为常引用
5. 在定义或声明函数时,我们可以将函数的形参指定为引用的形式,这样在调用函数时就会将实参和形参绑定在一起,让它们都指代同一份数据。
如此一来,如果在函数体中修改了形参的数据,那么实参的数据也会被修改,从而拥有“在函数内部影响函数外部数据”的效果。
一个能够展现按引用传参的优势的例子就是交换两个数的值
6.swap1() 直接传递参数的内容,不能达到交换两个数的值的目的。对于 swap1()来说,a、b是形参,是作用范围仅限于函数内部的局部变量,它们有自己独立的内存,和 num1、num2指代的数据不一样。调用函数时分别将num1、num2的值传递给 a、b,此后num1、num2和 a、b再无任何关系,在 swap1()内部修改 a、b的值不会影响函数外部的 num1、num2,更不会改变num1、num2的值。
swap2() 传递的是指针,能够达到交换两个数的值的目的。调用函数时,分别将 num1、num2的指针传递给 p1、p2,此后p1、p2指向 a、b所代表的数据,在函数内部可以通过指针间接地修改 a、b的值。
swap3() 是按引用传递,能够达到交换两个数的值的目的。调用函数时,分别将 a、b绑定到 num1、num2所指代的数据,此后 a和 num1、b和 num2就都代表同一份数据了,通过 a修改数据后会影响 num1,通过b 修改数据后也会影响num2。
以上代码的编写中可以发现,按引用传参在使用形式上比指针更加直观。在以后的 C++ 编程中,鼓励大家大量使用引用,它一般可以代替指针(当然指针在C++中也不可或缺),C++标准库也是这样做的。
8.引用作为函数返回值
在将引用作为函数返回值时应该注意一个小问题,就是不能返回局部数据(例如局部变量、局部对象、局部数组等)的引用,因为当函数调用完成后局部数据就会被销毁,有可能在下次使用时数据就不存在了,C++编译器检测到该行为时也会给出警告。
9.继承是类与类之间的关系,是一个很简单很直观的概念,与现实世界中的继承类似。
继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。例如类B 继承于类A,那么B 就拥有A 的成员变量和成员函数。被继承的类称为父类或基类,继承的类称为子类或派生类。派生类除了拥有基类的成员,还可以定义自己的新成员,以增强类的功能
10.以下是两种典型的使用继承的场景:
1) 当你创建的新类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承,这样不但会减少代码量,而且新类会拥有基类的所有功能。
2) 当你需要创建多个类,它们拥有很多相似的成员变量或成员函数时,也可以使用继承。可以将这些类的共同成员提取出来,定义为基类,然后从基类继承,既可以节省代码,也方便后续修改成员。
11. 继承的一般语法为:
class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};
实例 :
class Student: public People{
}
继承方式包括 public(公有的)、private(私有的)和protected(受保护的),此项是可选的,如果不写,那么默认为private。
1) public继承方式
基类中所有 public 成员在派生类中为 public 属性;
基类中所有 protected 成员在派生类中为 protected 属性;
基类中所有 private 成员在派生类中不能使用。
2) protected继承方式
基类中的所有 public 成员在派生类中为 protected 属性;
基类中的所有 protected 成员在派生类中为 protected 属性;
基类中的所有 private 成员在派生类中不能使用。
3) private继承方式
基类中的所有 public 成员在派生类中均为 private 属性;
基类中的所有 protected 成员在派生类中均为 private 属性;
基类中的所有 private 成员在派生类中不能使用。
继承方式/基类成员 |
public成员 |
protected成员 |
private成员 |
public继承 |
public |
protected |
不可见 |
protected继承 |
protected |
protected |
不可见 |
private继承 |
private |
private |
不可见
|
12.由于 private和 protected继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。
13.1) 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为 protected 时,那么基类成员在派生类中的访问权限最高也为protected,高于protected 的会降级为protected,但低于protected 不会升级。再如,当继承方式为public 时,那么基类成员在派生类中的访问权限将保持不变。
也就是说,继承方式中的 public、protected、private是用来指明基类成员在派生类中的最高访问权限的。
2) 不管继承方式如何,基类中的 private成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。
3) 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public或 protected;只有那些不希望在派生类中使用的成员才声明为private。
4)如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。
注意,我们这里说的是基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。
14.改变访问权限
使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将public 改为private、将private 改为public。
class Student: public People{
public:
void learning();
public:
using People::m_name; //将private改为public
using People::m_age; //将private改为public
float m_score;
private:
using People::show; //将public改为private
};
void Student::learning(){
cout<<"我是"<
}
代码中首先定义了基类 People,它包含两个protected 属性的成员变量和一个public 属性的成员函数。定义Student 类时采用public 继承方式,People类中的成员在 Student类中的访问权限默认是不变的。
15.C++继承时名字的遮蔽
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。
基类 People 和派生类Student 都定义了成员函数show(),它们的名字一样,会造成遮蔽。第37 行代码中,stu是 Student类的对象,默认使用 Student类的 show()函数。
但是,基类 People 中的 show() 函数仍然可以访问,不过要加上类名和域解析符
基类成员函数和派生类成员函数不构成重载
基类成员和派生类成员的名字一样时会造成遮蔽,这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。
Base 类的func()、func(int)和Derived 类的func(char *)、func(bool)四个成员函数的名字相同,参数列表不同,它们看似构成了重载,能够通过对象d 访问所有的函数,实则不然,Derive类的 func遮蔽了 Base类的 func,导致代码没有匹配的函数,所以调用失败。
如果说有重载关系,那么也是 Base 类的两个 func 构成重载,而Derive 类的两个func 构成另外的重载。
16.C++派生类的构造函数
基类的成员函数可以被继承,可以通过派生类的对象访问,但这仅仅指的是普通的成员函数,类的构造函数不能被继承。构造函数不能被继承是有道理的,因为即使继承了,它的名字和派生类的名字也不一样,不能成为派生类的构造函数,当然更不能成为普通的成员函数。
在设计派生类时,对继承过来的成员变量的初始化工作也要由派生类的构造函数完成,但是大部分基类都有 private属性的成员变量,它们在派生类中无法访问,更不能使用派生类的构造函数来初始化。
这种矛盾在C++继承中是普遍存在的,解决这个问题的思路是:在派生类的构造函数中调用基类的构造函数。
17.Student::Student(char *name, int age, float score): People(name, age), m_score(score){ }
People(name, age)就是调用基类的构造函数,并将 name和 age作为实参传递给它,m_score(score)是派生类的参数初始化表,它们之间以逗号,隔开。也可以将基类构造函数的调用放在参数初始化表后面:Student::Student(char *name, int age, float score): m_score(score), People(name, age){ }
但是不管它们的顺序如何,派生类构造函数总是先调用基类构造函数再执行其他代码(包括参数初始化表以及函数体中的代码),总体上看和下面的形式类似:
Student::Student(char *name, int age, float score){
People(name, age);
m_score = score;
}
函数头部是对基类构造函数的调用,而不是声明,所以括号里的参数是实参,它们不但可以是派生类构造函数参数列表中的参数,还可以是局部变量、常量等,例如:
Student::Student(char *name, int age, float score): People("小明", 16),m_score(score){ }
18.构造函数调用顺序
基类构造函数总是被优先调用,这说明创建派生类对象时,会先调用基类构造函数,再调用派生类构造函数,如果继承关系有好几层的话,例如:A --> B --> C
那么创建 C 类对象时构造函数的执行顺序为:
A类构造函数 --> B类构造函数--> C类构造函数
构造函数的调用顺序是按照继承的层次自顶向下、从基类再到派生类的。
还有一点要注意,派生类构造函数中只能调用直接基类的构造函数,不能调用间接基类的。以上面的 A、B、C类为例,C是最终的派生类,B就是 C的直接基类,A就是 C的间接基类。
C++ 这样规定是有道理的,因为我们在 C中调用了 B的构造函数,B又调用了 A的构造函数,相当于 C间接地(或者说隐式地)调用了 A的构造函数,如果再在 C中显式地调用 A的构造函数,那么 A的构造函数就被调用了两次,相应地,初始化工作也做了两次,这不仅是多余的,还会浪费CPU时间以及内存,毫无益处,所以C++ 禁止在C 中显式地调用A 的构造函数。
事实上,通过派生类创建对象时必须要调用基类的构造函数,这是语法规定。换句话说,定义派生类构造函数时最好指明基类构造函数;如果不指明,就调用基类的默认构造函数(不带参数的构造函数);如果没有默认构造函数,那么编译失败。
创建对象 stu1 时,执行派生类的构造函数Student::Student(),它并没有指明要调用基类的哪一个构造函数,从运行结果可以很明显地看出来,系统默认调用了不带参数的构造函数,也就是People::People()。
创建对象 stu2 时,执行派生类的构造函数Student::Student(char *name, int age, float score),它指明了基类的构造函数。
如果将People(name, age)去掉,也会调用默认构造函数,第37 行的输出结果将变为:
xxx的年龄是0,成绩是90.5。
如果将基类 People 中不带参数的构造函数删除,那么会发生编译错误,因为创建对象 stu1 时需要调用 People类的默认构造函数, 而 People类中已经显式定义了构造函数,编译器不会再生成默认的构造函数。
和构造函数类似,析构函数也不能被继承。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。
另外析构函数的执行顺序和构造函数的执行顺序也刚好相反:
创建派生类对象时,构造函数的执行顺序和继承顺序相同,即先执行基类构造函数,再执行派生类构造函数。
而销毁派生类对象时,析构函数的执行顺序和继承顺序相反,即先执行派生类析构函数,再执行基类析构函数。
19.C++类的多继承
在前面的例子中,派生类都只有一个基类,称为单继承(Single Inheritance)。除此之外,C++也支持多继承(Multiple Inheritance),即一个派生类可以有两个或多个基类。
多继承容易让代码逻辑复杂、思路混乱,一直备受争议,中小型项目中较少使用,后来的 Java、C#、PHP等干脆取消了多继承。】
多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D:
class D: public A, private B, protected C{
//类D新增加的成员
}
D 是多继承形式的派生类,它以公有的方式继承 A类,以私有的方式继承 B类,以保护的方式继承 C类。D 根据不同的继承方式获取 A、B、C中的成员,确定它们在派生类中的访问权限。
多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。以上面的 A、B、C、D类为例,D类构造函数的写法为:
D(形参列表): A(实参列表), B(实参列表), C(实参列表){
//其他操作
}
基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同。仍然以上面的 A、B、C、D类为例,即使将 D类构造函数写作下面的形式:
D(形参列表): B(实参列表), C(实参列表), A(实参列表){
//其他操作
}
那么也是先调用 A 类的构造函数,再调用B 类构造函数,最后调用C 类构造函数
多继承形式下析构函数的执行顺序和构造函数的执行顺序与单继承相同。
20.命名冲突
当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除二义性。
21.c++虚继承和虚基类
多继承(Multiple Inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。尽管概念上非常简单,但是多个基类的相互交织可能会带来错综复杂的设计问题,命名冲突就是不可回避的一个。
多继承时很容易产生命名冲突,即使我们很小心地将所有类中的成员变量和成员函数都命名为不同的名字,命名冲突依然有可能发生,比如典型的是菱形继承,如下图所示:
类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C,这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A-->B-->D 这条路径,另一份来自 A-->C-->D 这条路径。
在一个派生类中保留间接基类的多份同名成员,虽然可以在不同的成员变量中分别存放不同的数据,但大多数情况下这是多余的:因为保留多份成员变量不仅占用较多的存储空间,还容易产生命名冲突。假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B-->D 这条路径,还是来自 A-->C-->D 这条路径。
为了消除歧义,我们可以在 m_a 的前面指明它具体来自哪个类:
void seta(int a){ B::m_a = a; }
这样表示使用 B 类的 m_a。当然也可以使用 C 类的:
void seta(int a){ C::m_a = a; }
22.虚继承
为了解决多继承时的命名冲突和冗余数据问题,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。
在继承方式前面加上 virtual 关键字就是虚继承
虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类(Virtual Base Class),本例中的 A 就是一个虚基类。在这种机制下,不论虚基类在继承体系中出现了多少次,在派生类中都只包含一份虚基类的成员。
观察这个新的继承体系,我们会发现虚继承的一个不太直观的特征:必须在虚派生的真实需求出现前就已经完成虚派生的操作。在上图中,当定义 D 类时才出现了对虚派生的需求,但是如果 B 类和 C 类不是从 A 类虚派生得到的,那么 D 类还是会保留 A 类的两份成员。
换个角度讲,虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。
在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个类是虚基类,况且新类的开发者也无法改变已经存在的类体系。
C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。
23.虚基类成员的可见性
因为在虚继承的最终派生类中只保留了一份虚基类的成员,所以该成员可以被直接访问,不会产生二义性。此外,如果虚基类的成员只被一条派生路径覆盖,那么仍然可以直接访问这个被覆盖的成员。但是如果该成员被两条或多条路径覆盖了,那就不能直接访问了,此时必须指明该成员属于哪个类。
以菱形继承为例,假设 A 定义了一个名为 x 的成员变量,当我们在 D 中直接访问 x 时,会有三种可能性:
如果 B 和 C 中都没有 x 的定义,那么 x 将被解析为 A 的成员,此时不存在二义性。
如果 B 或 C 其中的一个类定义了 x,也不会有二义性,派生类的 x 比虚基类的 x 优先级更高。
如果 B 和 C 中都定义了 x,那么直接访问 x 将产生二义性问题。
可以看到,使用多继承经常会出现二义性问题,必须十分小心。上面的例子是简单的,如果继承的层次再多一些,关系更复杂一些,程序员就很容易陷人迷魂阵,程序的编写、调试和维护工作都会变得更加困难,因此我不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。也正是由于这个原因,C++ 之后的很多面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。