友情链接:C/C++系列系统学习目录
知识总结顺序参考C Primer Plus(第六版)和谭浩强老师的C程序设计(第五版)等,内容以书中为标准,同时参考其它各类书籍以及优质文章,以至减少知识点上的错误,同时方便本人的基础复习,也希望能帮助到大家
最好的好人,都是犯过错误的过来人;一个人往往因为有一点小小的缺点,将来会变得更好。如有错漏之处,敬请指正,有更好的方法,也希望不吝提出。最好的生活方式就是和努力的大家,一起奔跑在路上
文章目录
- 🚀一、继承和派生
- ⛳(一)概述
- 🎈1.派生和继承的实现
- 🎈2.派生类(子类)对象的内存分布
- ⛳(二)派生类和基类之间的特殊关系
- 🎈1.访问权限:继承和派生的三种方式
- 🎈2.指针指向
- ⛳(三)子类
- 🎈1.子类的构造函数
- 🎈2.子类的析构函数
- 🎈3.子类型
- ⛳(四)多重继承
- 🎈1.用法
- 🎈2.多重继承的弊端:二义性
- 🎈3.虚基类
🚀一、继承和派生
⛳(一)概述
父亲“派生”出儿子,儿子“继承”自父亲
从一个类派生出另一个类时,原始类称为基类,继承类称为派生类。派生和派生,本质是相同的,只是从不同的角度来描述。
🎈1.派生和继承的实现
Father.h
#pragma once
#include <string>using namespace std;class Father
{
public:Father(const char*name, int age);~Father();string getName();int getAge();string description();
private:int age;string name;
};
Father.cpp
#include "Father.h"
#include <sstream>
#include <iostream>Father::Father(const char*name, int age)
{cout << __FUNCTION__ << endl;this->name = name;this->age = age;
}Father::~Father()
{
}string Father::getName() {return name;
}
int Father::getAge() {return age;
}string Father::description() {stringstream ret;ret << "name:" << name << " age:" << age;return ret.str();
}
Son.h
#pragma once
#include "Father.h"
class Son : public Father {
public:Son(const char *name, int age, const char *game); //派生类需要自己的构造函数。~Son();string getGame();string description();
private:string game;
};
Son.cpp
#include "Son.h"
#include <iostream>
#include <sstream>// 创建Son对象时, 会调用构造函数!
// 会先调用父类的构造函数, 用来初始化从父类继承的数据
// 再调用自己的构造函数, 用来初始化自己定义的数据
Son::Son(const char *name, int age, const char *game) : Father(name, age) {cout << __FUNCTION__ << endl;// 没有体现父类的构造函数, 那就会自动调用父类的默认构造函数!!!// 如果父类没有默认构造函数,如此例,那就需要使用初始化列表,用儿子的两个参数来显示调用父类构造函数进行初始化this->game = game;
}Son::~Son() {
}string Son::getGame() {return game;
}string Son::description() {stringstream ret;// 子类的成员函数中, 不能访问从父类继承的private成员ret << "name:" << getName() << " age:" << getAge()<< " game:" << game;return ret.str();
}
main.cpp
#include <iostream>
#include "Father.h"
#include "Son.h"int main(void) {Father wjl("王健林", 68);Son wsc("王思聪", 32, "电竞");cout << wjl.description() << endl;// 子类对象调用方法时, 先在自己定义的方法中去寻找, 如果有, 就调用自己定义的方法// 如果找不到, 就到父类的方法中去找, 如果有, 就调用父类的这个同名方法// 如果还是找不到, 就是发生错误!cout << wsc.description() << endl;system("pause");return 0;
}
-
除了“构造函数”和“析构函数”,父类的所有成员函数,以及数据成员,都会被子类继承!
-
子类一般会添加自己的数据成员和成员函数, 或者, 重新定义从父类继承的方法!!! 子类对象就会调用自己重新定义的方法, 不会调用父类的同名方法,是一种静态的多态,即在派生类中重新定义基类的方法。还有一种动态的多态:虚方法,在后面会讲
注意:
-
如果子类调用父类的同名方法,方法中涉及到的数据也是父类中的并不是子类中的
-
重新定义从父类继承的方法并不是重载,即如果重新定义继承的方法,应确保与原来的原型完全相同,只是函数体实现不同,如果原型不同,例如:
class Father { public:virtual void showperks(int a) const; ... };class Son : public Father { public:virtual void showperks() const; ... };
新定义将showperks( )定义为一个不接受任何参数的函数。重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。
-
-
_FUNCTION_ 获取函数签名,假如它是一个成员函数,它的类名和const/volatile限定符也将是签名的一部分。
🎈2.派生类(子类)对象的内存分布
vs使用技巧:
设置 vs 编译器,在命令行中添加选项(打印指定类的内存分布):
/d1 reportSingleClassLayoutFather /d1 reportSingleClassLayoutSon
重新生成:
测试:
int main(void) {Father wjl("王健林", 68);Son wsc("王思聪", 32, "电竞");cout << sizeof(wjl.age) << endl;cout << sizeof(wjl.name) << endl;cout << sizeof(wjl) << endl; // 48cout << endl;cout << sizeof(wsc.age) << endl;cout << sizeof(wsc.name) << endl;cout << sizeof(wsc.getGame()) << endl;cout << sizeof(wsc) << endl; // 88system("pause");return 0;
}
- 注意涉及到内存对齐
- 成员函数不占用对象的内存空间,但是也被子类继承了!!!
⛳(二)派生类和基类之间的特殊关系
🎈1.访问权限:继承和派生的三种方式
(1)public(公有)继承 [使用最频繁]
父类中定义的成员(数据成员和函数成员)被继承后,访问权限不变!
-
public --> public
-
protected --> protected
-
private --> private
(2)private(私有)继承
父类中定义的成员(数据成员和函数成员)被继承后,访问权限都变成 private
-
public --> private
-
protected --> private
-
private --> private
(3)protected(保护)继承
-
public --> protected
-
protected --> protected
-
private --> private
小结:public 继承全不变,private 继承全变私,protected 继承只把 public 降级为 protected
子类对父类成员的访问权限:
无论通过什么方式(public、protected、private)继承,在子类内部均可访问父类中的 public、protected 成员。
private 成员不可访问(如果想要子类能够访问,就定义为 protected),使用构造函数不能直接设置继承的成员,而必须使用基类的公有方法来访问私有的基类成员。具体地说,派生类构造函数必须使用基类构造函数。
继承方式只影响外界通过子类对父类成员的访问权限。
-
public 继承,父类成员的访问权限全部保留至子类;
-
protected 继承,父类 public 成员的访问权限在子类中降至 protected;
-
private 继承,父类 public、protected 成员的访问权限在子类中均降至 private。
🎈2.指针指向
基类指针可以在不进行显式类型转换的情况下指向派生类对象,那么同样,基类引用可以在不进行显式类型转换的情况下引用派生类对象:
Father father("陈七",20);
Son son("陈启",18,"王者");Father * man = &son;
Father & man = son;
-
注意:基类指针或引用只能用于调用基类方法,这涉及到之后要讲的多态
-
通常,C++要求引用和指针类型与赋给的类型匹配,但这一规则对继承来说是例外。然而,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针
上述规则是有道理的。例如,如果允许基类引用隐式地引用派生类对象,则可以使用基类引用为派生类对象调用基类的方法。因为派生类继承了基类的方法,所以这样做不会出现问题。如果可以将基类对象赋给派生类引用,将发生什么情况呢?派生类引用能够为基对象调用派生类方法,这样做将出现问题。例如,将方法Son::getGame() 用于Father对象是没有意义的,因为Father对象没有rating成员。
-
在函数参数中使用派生类引用或指针,在传参时,可以传递基类对象,也可以传递派生类对象
-
能够将基类对象初始化为派生类对象:
Son son("陈七",18,"王者"); Father father(son);
该构造函数原型:
Father(const Son &);
类定义中没有这样的构造函数,但存在隐式复制构造函数:
Father(const Father &);
形参是基类引用,因此它可以引用派生类。这样,将father初始化为son时,将要使用该构造函数,它复制age和name成员。换句话来说,它将father初始化为嵌套在son中的Father对象。即用派生类对象初始化基类对象时,只有该派生类对象的基类部分会被拷贝,派生类部分将会被忽略掉。
同样,也可以将派生对象赋给基类对象:
Son son("陈七",18,"王者"); Father father; father = son;
在这种情况下,程序将使用隐式重载赋值运算符:
Father & operatpr=(const Father&) const;
-
在使用基类指针时,此指针调用的方法是基类的,即使指针指向子类,要使用子类的就涉及到多态问题
⛳(三)子类
🎈1.子类的构造函数
调用父类的哪个构造函数
class Son : public Father {public:// 在子类的构造函数中,显式调用父类的构造函数Son(const char *name, int age, const char *game):Father(name, age) {this->game = game;}// 没有显式的调用父类的构造函数,那么会自动调用父类的默认构造函数Son(const char *name, const char *game){this->game = game;}......
};
-
创建派生类对象时,程序首先创建基类对象。从概念上说,这意味着基类对象应当在程序进入派生类构造函数之前被创建。C++使用成员初始化列表语法来完成这种工作。例如:
此例中,需要提供两个相同的参数const char *name, int age来使用父类的构造函数,即Father(name, age) 是成员初始化列表,调用Father构造函数,Son构造函数将实参"王思聪"和2赋给形参name,age,然后将这些参数作为实参传递给Father构造函数,后者将创建一个嵌套Father对象,并将数据"王思聪"和2存储在该对象中。然后,程序进入Son构造函数体,完成Son对象的创建,并将参数game的值赋给game成员
-
如果省略成员初始化列表,必须首先创建基类对象,如果不调用基类构造函数,程序将使用默认的基类构造函数,除非要使用默认构造函数,否则应显式调用正确的基类构造函数。
-
也可以使用复制构造函数来作为初始化列表:
RatedPlayer::RatedPlayer(unsigned int r, const TableTennisPlayer & tp):TableTennisPlayer (tp) {rating = r; }
这里将TableTennisPlayer的信息传递给了TableTennisPlayer构造函数:
TableTennisPlayer (tp)
首先,由于tp的类型为TableTennisPlayer &,因此将调用基类的复制构造函数。基类没有定义复制构造函数,但编译器将自动生成一个。在这种情况下,执行成员复制的隐式复制构造函数是合适的,因为这个类没有使用动态内存分配(string成员确实使用了动态内存分配,但本书前面说过,成员复制将使用string类的复制构造函数来复制string成员)。
1.总之呢,构建子类必须要有个父类,要继承父类初始化好的数据,根据参数列表中使用哪种父类的构造函数,就需要在子类构造函数中提供对应的参数,
- 比如 Father(name, age)就需要提供const char *name, int age,子类构造函数体中管自己的game就行了,
- 或者TableTennisPlayer (tp)使用拷贝构造函数,就需要提供const TableTennisPlayer & tp参数,到时候传递个已经构造好的父类进来就行,
- 再或者不用参数化列表,会调用默认构造函数,参数只有一个自己的game就行
如果要想子类继承和我们定义好的父类实例一样的数据,可以都使用默认构造函数,或者子类的参数列表使用拷贝构造函数
2.实际上的初始化列表:
子类构造函数(数据类型 数值1,数据类型 数值2):父类构造函数(),变量名1(数值1),变量名2(数值2){}
- 在构造函数执行时,先执行初始化列表,实现变量的初始化,然后再执行函数内部的语句
- 成员初始化的顺序只与声明的顺序有关,而跟初始化列表的顺序无关。
- 当类成员中有引用和const常量时就一定得初始化,否则会报错,
3.注意:
先使用父类构造函数初始化父类数据成员,以便于子类继承,但只是初始化数据,并未创建一个父类实例对象(此问题暂定)
4.当使用已有子类对象来初始化子类对象时:
前面讲的初始化方式都是普通的创建子类,所以都是涉及到关于构造函数以及它的初始化列表使用哪种父类构造函数先初始化父类,但是这里就涉及初始化方式采用已有子类对象初始化,涉及到的是子类的拷贝构造函数,所以我们应在子类的拷贝构造函数中使用初始化列表方式,先决定使用哪种构造函数方式初始化父类
同样,如果不指定,会先调用父类的默认构造函数。再调用子类的拷贝构造函数
显示调用父类拷贝构造函数,则是先调用父类拷贝构造函数, 再调用子类的拷贝构造函数:
Son::Son(const Son& z):Father(z) {cout << "子类拷贝构造函数被调用";game = z.game; }
注意:我们使用子类构造函数时候,一般只会设置自己类的值,而不管从父类继承下来的值,这可能会导致我们拷贝时,最后的子类当中从父类继承下来的值不符合我们的预期,如下图,我们拷贝后的wsc1的age和name与wsc并不一样
因为我们并没有在子类拷贝构造函数中设置这两个值,用的是父类构造函数继承下来的两个值
Father::Father() {cout << "父类默认构造函数被调用" << endl;this->name = "陈三";this->age = 18; }Son::Son(const Son& z) {cout << "子类拷贝构造函数被调用";game = z.game;//name = z.name; 这样过后就不会有问题了//age = z.age; }int main(void) {Son wsc("王思聪", 32, "电竞");Son wsc1 = wsc;cout << wsc1.age << endl << wsc1.name << endl << wsc1.getGame() << endl;system("pause");return 0; }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J40pLML6-1687363674600)(E:\create\图片\C++\1\42.png)]
子类和父类的构造函数的调用顺序
当创建子类对象时, 构造函数的调用顺序:
静态数据成员的构造函数 -> 父类的构造函数 -> 非静态的数据成员的构造函数 -> 自己的构造函数
注意:
无论创建几个对象, 该类的静态成员只构建一次, 所以静态成员的构造函数只调用 1 次!!!
#include <iostream>
#include <Windows.h>using namespace std;class M {
public:M() {cout << __FUNCTION__ << endl;}
};class N {
public:N() {cout << __FUNCTION__ << endl;}
};class A {
public:A() {cout << __FUNCTION__ << endl;}
};class B : public A {
public:B() {cout << __FUNCTION__ << endl;}
private:M m1;M m2;static N ms;
};N B::ms; //静态成员int main(void) {B b;system("pause");
}
执行:
- N::N 静态数据成员的构造函数
- A::A 父类的构造函数
- M::M 非静态数据成员的构造函数
- M::M 非静态数据成员的构造函数
- B::B 自己的构造函数
🎈2.子类的析构函数
子类的析构函数的调用顺序,和子类的构造函数的调用顺序相反!!!记住,相反即可
#include <iostream>
#include <Windows.h>using namespace std;class M {
public:M() {cout << __FUNCTION__ << endl;}~M() {cout << __FUNCTION__ << endl;}
};class N {
public:N() {cout << __FUNCTION__ << endl;}~N() {cout << __FUNCTION__ << endl;}
};class A {
public:A() {cout << __FUNCTION__ << endl;}~A() {cout << __FUNCTION__ << endl;}
};class B : public A {
public:B() {cout << __FUNCTION__ << endl;}~B() {cout << __FUNCTION__ << endl;
}
private:M m1;M m2;static N ms;
};N B::ms; //静态成员int main(void) {{B b;cout << endl;}system("pause");
}
执行:
N::N
A::A
M::M
M::M
B::BB::~B
M::~M
M::~M
A::~A
🎈3.子类型
公有继承时,派生类的对象可以作为基类的对象处理,派生类是基类的子类型。
#include <iostream>using namespace std;
class A {
public:A() {}~A() {}void kill() { cout << "A kill." << endl; }
};class B : public A {
public:B(){}~B(){}void kill() { cout << "B kill." << endl; }
};void test(A a) {a.kill(); //调用的是A类对象的kill方法
}int main(void) {A a;B b;test(a); //A Killtest(b); //A Killsystem("pause");return 0;
}
-
子类型关系具有单向传递性:C 类是 B 类的子类型而 B 类是 A 类的子类型,C 类也是 A 类的子类型
-
如果把继承方式改为 protected 继承,或 private 继承,就会导致编译失败!
-
在需要父类对象的任何地方, 可以使用”公有派生”的子类的对象来替代,从而可以使用相同的函数统一处理基类对象和公有派生类对象
即:形参为基类对象时,实参可以是派生类对象(但反过来是不行的):
#include <iostream> #include <sstream>using namespace std;class Father { public:void play() {cout << "KTV唱歌!" << endl;} };class Son : public Father { public:void play() {cout << "今晚吃鸡!" << endl;} };void party(Father *f1, Father *f2) {f1->play();f2->play(); }int main(void) {Father yangKang;Son yangGuo;party(&yangKang, &yangGuo); //都将执行KTV唱歌!system("pause");return 0; }
-
子类型的应用:
//1.基类(父类)的指针,可以指向这个类的公有派生类(子类型)对象: Son yangGuo; Father * f = &yangGuo;//2. 公有派生类(子类型)的对象可以初始化基类的引用 Son yangGuo; Father &f2 = yangGuo;//3. 公有派生类的对象可以赋值给基类的对象 Son yangGuo; Father f1 = yangGuo;
如上所有的都是以父类为基础,都只能使用父类的相关方法,数据成员也只会将子类中包含的那个父类的所有数据给复制过来
⛳(四)多重继承
多继承/多重继承:
一个派生类可以有两个或多个基类(父类)。多重继承在中小型项目中较少使用,在 Java、C#等语言中直接取消多继承, 以避免复杂性.
🎈1.用法
将多个基类用逗号隔开.
实例:
例如已声明了类 A、类 B 和类 C,那么可以这样来声明派生类 D:
class D: public A, private B, protected C{//类 D 自己新增加的成员
};
D 是多继承形式的派生类,有 3 个父类(基类)它以公有的方式继承 A 类,以私有的方式继承 B 类,以保护的方式继承 C 类。根据不同的继承方式获取 A、B、C 中的成员.
多继承的构造函数:
D(形参列表): A(实参列表), B(实参列表), C(实参列表){//其他操作
}
Father.h
#pragma once
#include <string>class Father
{
public:Father(const char *lastName="无姓", const char *firstName="无名");~Father();void playBasketball(); //打篮球
protected:std::string lastName; //姓std::string firstName; //名
};
Father.cpp
#include "Father.h"
#include <iostream>Father::Father(const char *lastName, const char *firstName)
{this->lastName = lastName;this->firstName = firstName;
}Father::~Father()
{
}void Father::playBasketball() {std::cout << "呦呦, 我要三步上篮了!" << std::endl;
}
Mother.h
#pragma once
#include <string>class Mother
{
public:Mother(const char * food,const char *lastName = "无姓", const char *firstName = "无名");~Mother();void dance();
private:std::string lastName; //姓std::string firstName; //名std::string food; //喜欢的食物
};
Mother.cpp
#include "Mother.h"
#include <iostream>Mother::Mother(const char *food,const char *lastName, const char *firstName)
{this->food = food;this->lastName = lastName;this->firstName = firstName;
}Mother::~Mother()
{
}void Mother::dance()
{std::cout << "一起跳舞吧, 一二三四, 二二三四..." << std::endl;
}
Son.h
#pragma once
#include "Father.h"
#include "Mother.h"class Son : public Father, public Mother {
public:Son(const char *lastName, const char *firstName,const char *food,const char *game);~Son();void playGame();
private:std::string game;
};
Son.cpp
#include "Son.h"
#include <iostream>Son::Son(const char *lastName, const char *firstName,const char *food,const char *game):Father(lastName, firstName), Mother(food)
{this->game = game;
}Son::~Son()
{
}void Son::playGame()
{std::cout << "一起玩" << game << "吧..." << std::endl;
}
main.cpp
#include <Windows.h>
#include "Son.h"int main(void) {Son wsc("川菜", "王", "思聪", "电竞");wsc.playBasketball();wsc.dance();wsc.playGame();system("pause");return 0;
}
多继承的构造函数的调用顺序:
基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,而是和声明派生类时基类出现的顺序相同:
//先调用Father的构造函数,再调用Mother的
class Son : public Father, public Mother
{
}
🎈2.多重继承的弊端:二义性
如果我们在父亲类中增加一个跳舞方法:
Father.h
#pragma once
#include <string>class Father
{
public:Father(const char *lastName="无姓", const char *firstName="无名");~Father();void playBasketball(); //打篮球void dance(); //跳舞
protected:std::string lastName; //姓std::string firstName; //名
};
Father.cpp
#include "Father.h"
#include <iostream>Father::Father(const char *lastName, const char *firstName)
{this->lastName = lastName;this->firstName = firstName;
}Father::~Father()
{
}void Father::playBasketball() {std::cout << "呦呦, 我要三步上篮了!" << std::endl;
}void Father::dance() {std::cout << "嘿嘿, 我要跳霹雳舞!" << std::endl;
}
Son.h
#pragma once
#include "Father.h"
#include "Mother.h"class Son : public Father, public Mother {
public:Son(const char *lastName, const char *firstName,const char *food,const char *game);~Son();void playGame();void dance(); //增加个跳舞方法
private:std::string game;
};
Son.cpp
#include "Son.h"
#include <iostream>Son::Son(const char *lastName, const char *firstName,const char *food,const char *game):Father(lastName, firstName), Mother(food)
{this->game = game;
}Son::~Son()
{
}void Son::playGame()
{std::cout << "一起玩" << game << "吧..." << std::endl;
}void Son::dance() {Father::dance();Mother::dance(); //这里同时调用父母的dance()方法std::cout << "霍霍, 我们来跳街舞吧! " << std::endl;
}
main.cpp
#include <Windows.h>
#include "Son.h"int main(void) {Son wsc("川菜", "王", "思聪", "电竞");wsc.playBasketball();// 解决多重继承的二义性的方法1:// 使用 "类名::" 进行指定, 指定调用从哪个基类继承的方法!wsc.Father::dance();wsc.Mother::dance();// 解决多重继承的二义性的方法2:// 在子类中重新实现这个同名方法, 并在这个方法内部, 使用基类名进行限定,// 来调用对应的基类方法wsc.dance();wsc.playGame();system("pause");return 0;
}
🎈3.虚基类
多重继承除了在同名方法上存在二义性之外,在同名数据成员上也存在二义性问题
#include <iostream>
#include <string>
#include <Windows.h>
using namespace std;// 电话类
class Tel {
public:Tel() {this->number = "未知";}
protected:string number; //电话号码;
};// 座机类
class FixedLine : public Tel {
};// 手机类
class MobilePhone :public Tel {
};// 无线座机
class WirelessTel :public FixedLine, public MobilePhone {
public:void setNumber(const char *number) {//this->number = number; //错误, 指定不明确this->FixedLine::number = number; //this可以省略}string getNumber() {//return MobilePhone::number;return MobilePhone::number;}
};int main(void) {WirelessTel phone;phone.setNumber("13243879166"); //这里设置的是继承自FixedLine的numbercout << phone.getNumber() << endl; //要获取继承自MobilePhone的number,打印未知system("pause");return 0;
}
座机和手机都继承自电话类,都继承了一个number数据成员,而无线座机有多重继承自座机和手机,将它们各自的number都继承了下来,以至于无线座机类有两个number数据成员,造成二义性
解决方法:
(1)使用基类名进行限定
(2)使用虚基类和虚继承:
#include <iostream>
#include <string>
#include <Windows.h>
using namespace std;// 电话类
class Tel {
public:Tel() {this->number = "未知";}
protected:string number; //电话号码;
};// 座机类
class FixedLine : virtual public Tel {
};// 手机类
class MobilePhone :virtual public Tel {
};// 无线座机
class WirelessTel :public FixedLine, public MobilePhone {
public:void setNumber(const char *number) {this->number = number; //直接访问number}string getNumber() {return this->number; //直接访问number}
};int main(void) {WirelessTel phone;phone.setNumber("13243879166"); cout << phone.getNumber() << endl; system("pause");return 0;
}
- 座机和手机都虚继承自电话类,此时他们共同的基类,就成为虚基类
- 而如果改成虚继承,由这两个派生出的子类就会只有一个number成员,从本质上说,两个子类对象共享一个number成员,
小结:尽量不要使用多重继承(多继承),部分高级语言已停止使用多重继承