精华内容
下载资源
问答
  • 近期在网上搜寻了一下关于C++派生类构造函数的调用... Lippman的>一书(主要是其中5.2节“继承体系下的对象构造”的叙述),对于一个派生类的构造函数,其中包含了基类构造、成员构造和自定义的代码等部分(忽略与本

        近期在网上搜寻了一下关于C++派生类构造函数的调用问题,大部分博文给出的顺序是:

    1、基类构造

    2、成员构造

    3、派生构造。

    这个顺序严格来讲是错误的,对新手极具误导性!


        依据侯捷翻译的Stanley B. Lippman的<<深度探索C++对象模型>>一书(主要是其中5.2节“继承体系下的对象构造”的叙述),对于一个派生类的构造函数,其中包含了基类构造、成员构造和自定义的代码等部分(忽略与本讨论不太相关的this指针、虚函数表指针等),其代码安排顺序为:

    1、(虚继承的)基类构造

    2、(普通继承的)基类构造

    3、设定虚表指针

    4、成员构造

    5、自己写的代码


    真正生成的派生类的构造函数的形式如下:(忽略关于this指针、虚函数表指针的代码)

    //CDerive为派生类,CVBase为虚继承的基类,CBase为基类,member为CMember类型的成员

    CDerive::CDerive()

    {

        CVBase::CVBase();

        CBase::CBase();

        member.CMember::CMember();

        //自己的代码

    }


    由此可以看出,基类构造的调用是在派生构造之中而不是之后,派生构造的调用与基类构造的调用是包含关系而不是先后关系。对“函数调用”的理解通常是:跳转到函数入口地址并执行其后的代码,若以此说来,真正的调用顺序为:

    1、派生构造

    2、基类构造

    3、成员构造


    经调试,实际的代码执行结果符合Lippman的描述,函数调用的堆栈顺序:

    1、派生构造进入

    2、基类构造进入

    3、基类构造返回

    4、成员构造进入

    5、成员构造返回

    6、派生构造返回


    下面贴几张调试时的图片:(OS: Win7x64  IDE: VS2010)


    继承关系如图


    1、首先进入派生类构造函数的断点


    2、进入了虚继承的基类构造的断点。按照继承时的顺序,CVBase在CBase之后的,但因为是虚继承所以先执行它的构造


    3、执行完CVBase的构造后接着执行CBase的构造


    4、最后执行CInner的构造


    5、后面将执行CDerive构造中自己写的代码了,但调试时忘了给这一步截图,这里也就没图可贴了 0.0~



    展开全文
  • 继承与派生(小题)

    2020-06-27 16:46:28
    以下关于C++语言中继承的叙述中,错误的是( D )。 A.继承是父类和子类之间共享数据和方法的机制 B.继承定义了一种类与类之间的关系 C.继承关系中的子类将拥有父类的全部属性和方法 D.继承仅仅允许单继承,即不允许...

    单选题:

    2-1
    一个类的私有成员 ( B )
    A.只能被该类的成员函数访问
    B.只能被该类的成员函数和友元函数访问
    C.只能被该类的成员函数、友元函数和派生类访问
    D.以上答案都不对

    2-2
    以下关于C++语言中继承的叙述中,错误的是( D )。
    A.继承是父类和子类之间共享数据和方法的机制
    B.继承定义了一种类与类之间的关系
    C.继承关系中的子类将拥有父类的全部属性和方法
    D.继承仅仅允许单继承,即不允许一个子类有多个父类

    2-3
    继承机制的作用是 ( C )
    A.信息隐藏
    B.数据封装
    C.定义新类
    D.数据抽象

    2-4
    在公有继承的情况下,在派生类中能够访问的基类成员包括 ( D )
    A.公有成员
    B.保护成员
    C.公有成员、保护成员和私有成员
    D.公有成员和保护成员

    2-5
    派生类继承基类的方式有 ( D )
    A.public
    B.private
    C.protected
    D.以上都对

    2-6
    在c++中,类之间的继承关系具有( C )。
    A.自反性
    B.对称性
    C.传递性
    D.反对称性

    2-7
    下列关于类的继承描述中,( D )是正确的。
    A.派生类公有继承基类时,可以访问基类的所有数据成员,调用所有成员函数。
    B.派生类也是基类,所以它们是等价的。
    C.派生类对象不会建立基类的私有数据成员,所以不能访问基类的私有数据成员。
    D.一个基类可以有多个派生类,一个派生类可以有多个基类。

    2-8
    下列有关继承和派生的叙述中,正确的是: ( D )
    A.派生类不能访问通过私有继承的基类的保护成员
    B.多继承的虚基类不能够实例化
    C.如果基类没有默认构造函数,派生类就应当定义带形参的构造函数
    D.类的的析构函数和构造函数都不能够被继承

    2-9
    下列程序的执行结果为 ( C )

    #include <iostream>
    using namespace std;
    
    class A {
    public:
        A() {     cout << "1";    }
        ~A() {    cout << "2";    }
    };
    class B: public A {
    public:
        B() {    cout << "3";    }
        ~B() {    cout << "4";    }
    };
    int main() {
        B b;
        return 0;
    }
    

    A.1234
    B.1324
    C.1342
    D.3142

    2-10
    下列关于派生类构造函数和析构函数的说法中,错误的是 ( D )
    A.派生类的构造函数会隐含调用基类的构造函数
    B.如果基类声明了带有形参表的构造函数,则派生类就应当声明构造函数
    C.在建立派生类对象时,先调用基类的构造函数,再调用派生类的构造函数
    D.在销毁派生类对象时,先调用基类的析构函数,再调用派生类的析构函数

    2-11
    建立派生类对象时, 3种构造函数分别是a(基类的构造函数)、b(成员对象的构造函数)、c(派生类的构造函数),这3种构造函数的调用顺序为 ( A )
    A.abc
    B.acb
    C.cab
    D.cba

    2-12
    下面叙述不正确的是 ( A )
    A.基类的保护成员在派生类中仍然是保护的成员
    B.基类的保护成员在公有派生类中仍然是保护的
    C.基类的保护成员在私有派生类中仍然是私有的
    D.对基类成员的访问必须是无二义性

    2-13
    下列关于继承的描述中,错误的是( D )。
    A.析构函数不能被继承
    B.派生类是基类的组合
    C.派生类的成员除了它自己的成员外,还包含了它的基类的成员
    D.派生类中继承的基类成员的访问权限到派生类保持不变

    2-14
    下面关于类的继承与派生的程序,其输出结果是 ( C )

    #include<iostream>
    using namespace std;
    class A 
    {
    public:
        A(){cout<<"A";}
    };
    class B
    {
    public:
        B(){cout<<"B";}
    };
    class C:public A
    {
        B b;
    public:
        C(){cout<<"C";}
    };
    int main(){
        C obj;
        return 0;
    }
    

    A.CBA
    B.BAC
    C.ACB
    D.ABC

    2-15
    可以用p.a的形式访问派生类对象p的基类成员a, 其中a是 ( D )
    A.私有继承的公有成员
    B.公有继承的私有成员
    C.公有继承的保护成员
    D.公有继承的公有成员

    2-16
    下面关于类的继承与派生的程序,其输出结果是 ( B )

    #include<iostream>
    using namespace std;
    class A {
    public:
        A(int i) {    x = i;  }
        void dispa() {
            cout << x << ',';
        }
    private:
        int x;
    };
    class B: public A {
    public:
        B(int i) : A(i + 10) {
            x = i;
        }
        void dispb() {
            dispa();
            cout << x << endl;
        }
    private:
        int x;
    };
    int main() {
        B b(2);
        b.dispb();
        return 0;
    }
    

    A.10,2
    B.12,10
    C.12,2
    D.2,2

    2-17
    若obj是类D的对象,则下列语句中正确的是 ( D )

    class B{
    private: void fun1(){ }
    protected: void fun2(){ }
    public: void fun3(){ }
    };
    class D : public B {
    protected: void fun4(){ }
    };
    

    A.obj.fun1();
    B.obj.fun2();
    C.obj.fun4();
    D.obj.fun3();

    2-18
    下面关于继承和派生的构造函数和析构函数的程序,输出结果是 ( A )

    #include<iostream>
    using namespace std;
    class AA {
    public:
        AA() { cout << "A"; }
        ~AA() { cout << "a"; }
    };
    class BB: public AA {
        AA aa;
    public:
        BB() { cout << "B"; }
        ~BB() { cout << "b"; }
    };
    int main() {
        BB bb;
        return 0;
    }
    

    A.AABaab
    B.AABbaa
    C.BAAaab
    D.BAAbaa

    展开全文
  • 我们在学习面向对象的这种编程方法的时候,常常会听到这三个词,封装、继承、派生,这也是面向对象编程的三大特性,在本节我们将依次阐述封装、继承、派生的具体用法,在这里,我们先叙述的是封装这个属性的的相关...

    前言

    在上述教程中,我们已经完成了 C++相对于 C语言来说独特的语法部分,在接下来的教程中,我们将叙述 C++中面向对象的语法特性。我们在学习面向对象的这种编程方法的时候,常常会听到这三个词,封装、继承、派生,这也是面向对象编程的三大特性,在本节我们将依次阐述封装、继承、派生的具体用法,在这里,我们先叙述的是封装这个属性的的相关内容。下图是关于 封装 这个特性所包含的一些内容。

    封装

    下图就是封装所具备的相关特性:

    image-20210209204824118

    那么上图所示的抽象出数据成员以及成员函数具体的含义是什么呢,正如前面教程所述,在前面的教程里,我们选用一个 Person类来作为例子进行讲解,其中这个类里我们有 name以及age,这个也就是我们抽象出来的数据,那抽象出来的成员函数也就是前面教程讲到的setName()setAge()函数,在设计这个类的时候,会把这个类的一些成员设置为私有的或者公有的,这也就是访问控制。具体的代码如下所示:

    /* 为了代码简便,省略相关构造函数以及析构函数,为的是展示封装的特性*/
    class Person {
    private:
    	char *name;
    	int age;
    
    public:
        
        Person()
        {
            cout << "Person" << endl;
            name = NULL;
        }
        
        ~Person()
        {
            cout << "~Person()" << endl;
            if (this->name)
            {
                delete this->name;
            }
        }
        
    	void setName(char *name)
    	{
    		if (this->name) {
    			delete this->name;
    		}
    		this->name = new char[strlen(name) + 1];
    		strcpy(this->name, name);
    	}
        
    	int setAge(int a)
    	{
    		if (a < 0 || a > 150)
    		{
    			age = 0;
    			return -1;
    		}
    		age = a;
    		return 0;
    	}
    };
    

    继承

    继承的含义就如其字面意思一样,用更加专业的话来说,就是从基类继承相关属性,而这个新的类就叫做派生类。下面这个示意图也表明了继承所带来的代码的简洁与方便。

    image-20210209211013964

    就如上述这张图所示,一个人肯定具有名字和年龄这两个属性,那作为一个学生来讲,他也必定具备名字和年龄这两个属性,那这个时候是要在 Student类里重新定义这些属性么?显然,因为引入了继承这个特性,只需要继承Person类,那么Student就具备 Person类的相关属性。在上述代码的基础上,我们增加如下所示的代码:

    /* 注意是在上述代码的基础上 */
    
    class Student : public Person
    {
        
    };
    
    int main(int argc, char **argv)
    {
        Student s;
        
        s.setName("zhangsan");
        s.setAge(16);
        s.printInfo();
    
        return 0;
    }
    

    上述代码中,Student类是继承自 Person类的,我们可以看到在上述所示的Student类中,并没有setNamesetAge的成员函数,但是在定义的 Student实例中,却能够适用 setNamesetAge的成员函数,这也就说明了 Student类已经继承了 Person类。

    继承后的访问控制

    private

    一个派生类从一个基类继承而来,而继承的方式有多种,可以是私有继承,也可以是公有继承,同时也可以是保护继承。那么这个时候基类的各个数据成员的访问属性又是怎么样的呢,我们来看一下下面这张图,其展现了以各种方式继承自基类的派生类的数据成员的属性。

    image-20210209223145289

    从这个表可以清楚地知道基类的访问属性与派生类的访问属性的对应情况。同样的,我们用一个简单的例子来说明这个知识点:

    class Father
    {
    private:
        int money;
    public:
        void it_skill(void)
        {
            cout << "The father's it skill" <<endl;
        }
    
        int getMoney(void)
        {
            return money;
        }
    
        void setMoney(int money)
        {
            this->money = money;
        }
    };
    

    这个是基类的数据成员以及成员函数,为了更好的说明继承后的数据的属性,我们定义一个 son类,代码如下所示:

    class Son : public Father
    {
    private:
        int toy;
    public:
        void play_game(void)
        {
            cout << "play_game()" << endl;
    
            int m;
    
            //money -= 1; /* 错误的代码 */
    
            m = getMoney();
            m--;
            setMoney(m);
        }
    };
    

    上述定义了两个类,一个是 Father类,一个是 Son类,Son类继承于 Father类,这两个类用通俗的语言进行解释便是,父亲有自己的私房钱,儿子有自己的玩具,父亲有一项技能是 it,儿子呢比较喜欢玩游戏。因为是继承,所以儿子类具有父亲类的相关属性,但是,作为儿子是不能够直接去父亲兜里拿钱的,那会被揍,但是如果儿子需要钱,可以向父亲要。这对应的代码也就是上述中 money -= 1,但是这是错误的,不能直接从父亲的兜里拿钱,而剩余的三句代码的意思也就相当于是向父亲要钱。用专业的话来讲也就是:派生类不能够访问基类的私有成员,紧接着是主函数的代码:

    int main(int argc, char **argv)
    {
        Son s;
    
        s.it_skill();
        s.setMoney(10);
        cout << "The money is:" << s.getMoney() << endl;
    
        s.play_game();
    
        return 0;
    }
    

    代码输出的结果如下所示:

    image-20210209232507917

    protected

    还是采用比较通俗的话来叙述这一知识点,儿子相对于父亲的关系自然是与其他人有所不同的,比如有一把父亲房间门的钥匙,对于儿子来说是可以拿到的,但是对于外人来说,这是不可访问的。那在程序中要如何实现这么一个功能呢?这里就要引入 protected了。代码如下所示:

    class Father {
    private:
    	int money;
    
    protected:
    	int room_key;   /* 增添的 room_key */
    	
    public:
    	void it_skill(void)
    	{
    		cout<<"father's it skill"<<endl;
    	}
    
    	int getMoney(void)
    	{
    		return money;
    	}
    
    	void setMoney(int money)
    	{
    		this->money = money;
    	}
    };
    

    我们可以看到在 Father类中,增添了一项就是 protected修饰的 room_key,紧接着我们来看Son类的代码:

    class Son : public Father {
    private:
    	int toy;
    public:
    	void play_game(void)
    	{
    		int m;
    		
    		cout<<"son paly game"<<endl;
    
    		m = getMoney();
    		m--;
    		setMoney(m);
    
    		/* 外人不能拿父亲的房间钥匙
    		 * 儿子可以
    		 */
    		room_key = 1; 
    	}
    };
    

    我们看到,这个时候,是可以在 Son类里面直接操作使用 protected修饰的 room_key的。在这里总结一下就是:派生类可以直接访问到基类用 protected 修饰的数据成员。接下来,我们继续看主函数的代码:

    int main(int argc, char **argv)
    {
    	Son s;
    
    	s.setMoney(10);
    	cout << s.getMoney()<<endl;
    
    	s.it_skill();
    	s.play_game();	
    
    	//s.room_key = 1;
    	
    	return 0;
    }
    

    通过上述代码可以看到 s.room_key = 1这条语句被注释了,这条语句是错误的,虽然基类使用了 protected修饰了 room_key,但是在主函数中,仍然是不能够直接访问 room_key的。

    调整访问控制

    依旧采用比较通俗的话来阐述,如果儿子从父亲那里继承了一些东西,那这个时候,继承得到的这些东西的处理权就全在儿子了。在程序里面也是同样的道理,我们在上述代码的基础上进行更改,Father类不变,改变 Son类。代码如下所示:

    class Son : public Father {
    private:
    	int toy;
    public:
    	using Father::room_key;
    	
    	void play_game(void)
    	{
    		int m;
    		
    		cout<<"son paly game"<<endl;
            
    		m = getMoney();
    		m--;
    		setMoney(m);
    
    		room_key = 1; 
    	}
    };
    

    上述代码中,我们可以看到在 public的作用域内,我们使用 using Father::room_keyroom_key的属性更改为 public,做了这样的更改之后,我们就可以在主函数里直接访问 room_key了。代码如下所示:

    int main(int argc, char **argv)
    {
    	Son s;
    
    	s.setMoney(10);
    	cout << s.getMoney()<<endl;
    
    	s.it_skill();
    	s.play_game();
    
    	s.room_key = 1;
    	
    	return 0;
    }
    

    上述代码是可以运行的,也说明这种方式是可行的。但是如果想要将 money的属性更改为 public,也就是增加如下所示的代码:

    class Son : public Father {
    private:
    	int toy;
    public:
    	using Father::room_key;
        using Father::money;
    	
    	void play_game(void)
    	{
    		int m;
    		
    		cout<<"son paly game"<<endl;
            
    		m = getMoney();
    		m--;
    		setMoney(m);
    
    		room_key = 1; 
    	}
    };
    

    那么编译将不会通过,错误信息如下所示:

    image-20210210001456319

    说明这种方法是不可行的,这是为什么呢?是因为对于 Son来说,money本身就是它不能访问到的数据,那么自然也就不能够对其属性进行更改了。换句更加专业的话来叙述也就是:在调整访问控制的时候,只有类本身能够访问到的数据才能调整它的访问控制,如果其本身对于这个类就是不能够访问的,那么也就无法对其进行更改

    那上述可以说是提升访问控制,同样的,也可以降低访问控制,比如说上述的 it_skill,如果不想把这个属性继续继承下去或者说不让外部能够访问到它,那么也可以降低它的访问控制,降低的方法跟提升的方法是一样的,只需要在 private中加上一句代码就可以,加了的代码如下所示:

    class Son : public Father
    {
    private:
        int toy;
        using Father::it_skill;
    public:
        /* 省略 */
    };
    

    因此,只要对于派生类能够看到的数据成员或者成员函数,它都能够提高或者降低它的访问控制。

    三种不同继承方式的差异

    在上述的内容中,我们提到了派生类在继承基类的时候,存在不同的继承方式,不同的继承方式对数据成员的使用以及其成员函数的调用存在不同的影响,下面分别是三种不同的继承方式:publicprivate以及protected,代码如下所示:

    /* 以 public 方式继承 */
    class Son_pub : public Father {
    private:
    	int toy;
    public:
    	
    	void play_game(void)
    	{
    		int m;
    		
    		cout<<"son play game"<<endl;
            
    		m = getMoney();
    		m--;
    		setMoney(m);
    
    		room_key = 1; 
    	}
    };
    
    /* 以 private 方式继承 */
    class Son_pri : private Father {
    private:
    	int toy;
    public:
    	
    	void play_game(void)
    	{
    		int m;
    		
    		cout<<"son play game"<<endl;
    		m = getMoney();
    		m--;
    		setMoney(m);
    
    		room_key = 1; 
    	}
    };
    
    /* 以 protected 方式继承 */
    class Son_pro : protected Father {
    private:
    	int toy;
    public:
    	
    	void play_game(void)
    	{
    		int m;
    		
    		cout<<"son play game"<<endl;
    		m = getMoney();
    		m--;
    		setMoney(m);
    
    		room_key = 1; 
    	}
    };
    

    上述代码就是以三种不同方式从 Father类得到的 Son类,每一种继承方式存在什么不同呢,我们通过主函数来说明这个问题:

    int main(int argc, char **argv)
    {
    	Son_pub s_pub;
    	Son_pro s_pro;
    	Son_pri s_pri;
    
    	s_pub.play_game();
    	s_pro.play_game();
    	s_pri.play_game();
    
    	s_pub.it_skill();
    	//s_pro.it_skill();  // error
    	//s_pri.it_skill();	 // error
    
    	return 0;
    }
    

    通过上述代码,并对照上述那种表,我们可以知道,无论是何种继承方式,派生类内部public的成员函数都是可以使用的,而对于从基类继承得到的成员函数,如果是以 protectedprivate方式来继承的话,那么是不能够在主函数进行调用的,因此上述代码中注释掉的两句后面表明了错误。

    上述的代码所展示的是一层的继承,我们在继承得到的派生类 Son的基础上继续继承得到 Grandson,首先我们先在 Father类里新增加一个public的数据成员,增加的代码如下所示:

    class Father
    {
    private:
        int money;
    protected:
        int room_key;
    public:
        int address;
        /*其余不改动,省略*/
    }

    增加了上述Father类的代码之后,我们来看 Grandson_pub类的代码:

    class Grandson_pub : public Son_pub
    {
    public:
        void test(void)
        {
            room_key = 1; /* room_key is protected */
            address = 2;  /* address is public */
        }
    };
    

    上述代码中,Grandson_pub是以 public的方式从 Son_pub继承而来,room_keyFather类是 protected,在 Son_pub类也是 protected,那么在这里也是 protected,而对于 address来说,它在 Father类里是 public,在 Son_pub里也是 public,在这里也是 public,所以在这里都能够访问到。

    紧接着来看,Grandson_pro类的代码:

    class Grandson_pro : public Son_pro
    {
    public:
        void test(void)
        {
            room_key = 1;  /* room_key is protected */
            address = 2;   /* address is protected */
        }
    };
    

    上述中,Grandson_pro是以 public的方式从 Son_pro中继承得到的,以刚刚那种分析的思路我们能够分析得出 room_key当前是 protected以及 addressprotected,那么当前的数据成员在这也就是都能够访问的了。

    继续来看Grandson_pri类的代码,代码如下所示:

    class Grandson_pri : public Son_pri
    {
    public:
        void test(void)
        {
            //room_key = 1; /* room_key is private */
            //address = 2;  /* address is private */
        }
    };
    

    上述中,Grandson_pri是以 public的方式从 Son_pri中继承得来,同样按照上述的分析方法,我们能够分析出 room_keyaddress都是 private的,既然是 private的,那么也就不能够进行访问,因此上述代码中,我们将两句代码进行了注释。

    小结

    上述就是本次分享的关于封装以及继承的相关内容,主要是关于继承之后数据成员的访问控制,以及通过不同的方式进行继承时的数据成员的访问控制。

    上述教程所涉及的代码可以通过百度云链接的方式获取到,下面是百度云链接:

    链接:https://pan.baidu.com/s/18AGYqxkxsEcR4ZW6_Nhevg
    提取码:dlst

    展开全文
  • 在上一则教程中,叙述关于C++类型转换相关内容,在本节教程中,将叙述 C++另一个内容,也就是抽象,这也是 C++相对于 C语言来说独特一点,下面我们就来着重叙述这一点。 纯虚函数 在介绍抽象类之前,需要弄...

    前言

    在上一则教程中,叙述了关于C++类型转换的相关内容,在本节教程中,将叙述 C++的另一个内容,也就是抽象,这也是 C++相对于 C语言来说独特的一点,下面我们就来着重叙述这一点。

    纯虚函数

    在介绍抽象类之前,需要弄明白何为纯虚函数,下面假定我们有这样一个需求:

    做一个“各个国家的人的调查”,调查各个国家的人的:饮食、穿衣、开车

    要完成这样一个事情,那我们现在就需要实现这样几个类,一个是 Human类,其他国家的人从 Human类里派生而来,就比如说是ChineseEnglishman,我们再回过头来想,我们所要实现的需求是调查各个国家的人,那么这个过程中,由Human类派生得到 ChineseEnglishman,那么在实例化对象的时候,我们实际上是不会用到Human类去定义一个对象的,考虑到这层因素,我们在 Human类里使用到了纯虚函数的概念,类实现的代码如下所示:

    class Human
    {
    private:
        int a;
    public:
        /*纯虚函数*/
    	virtual void eating(void) = 0;
    	virtual void wearing(void) = 0;
    	virtual void driving(void) = 0;
    };
    
    class Englishman : public Human
    {
    public:
        void eating(void)  { cout<<"use knife to eat"<<endl; }
    	void wearing(void) {cout<<"wear english style"<<endl; }
    	void driving(void) {cout<<"drive english car"<<endl; }
    };
    
    class Chinese : public Human 
    {
    public:
    	void eating(void)  { cout<<"use chopsticks to eat"<<endl; }
    	void wearing(void) {cout<<"wear chinese style"<<endl; }
    	void driving(void) {cout<<"drive chinese car"<<endl; }
    };
    

    我们可以看到在上述的代码中,Human类的成员函数跟前几讲所写的成员函数有所不同,而如上述 Human类的成员函数这般写法,也就被称之为是纯虚函数

    抽象类

    上述引出了纯虚函数的写法,那纯虚函数和抽象类之间有什么关系呢?实际上,抽象类就是具有纯虚函数的类,那这抽象类存在的意义是什么呢?总的来说,其作用也就是:向下定义好框架,向上提供统一的接口,其不能够实例化对象,基于上述几个类的前提下,我们编写主函数的代码:

    int main(int argc,char **argv)
    {
        Human h;
        
        return 0;
    }
    

    因为抽象类不能够实例化对象,所以上述代码编译结果是错误的,错误信息如下所示:

    而使用通过抽象类派生得到的派生类实例化对象是可行的,代码如下所示:

    int main(int argc, char** argv)
    {
        Englishman e;
    	Chinese    g;
        
        return 0;
    }
    

    另外需要注意的是:在派生抽象类的过程中,如果派生得到的子类没有覆写所有的纯虚函数,那么这个子类还是抽象类,比如有如下所示的代码,Human类沿用的是上述的写法,代码不变,如果我们将上述的 Chinese类进行更改,更改后的代码如下所示:

    class Chinese : public Human 
    {
    public:
    	void eating(void) { cout<<"use chopsticks to eat"<<endl; }
    	void wearing(void) {cout<<"wear chinese style"<<endl; }
    	//void driving(void) {cout<<"drive chinese car"<<endl; }
    };
    

    如上述代码所示,我们将 driving()函数注释掉了,那么也就是说,我们并没有将抽象类的全部纯虚函数进行覆写,那么当前这个Chinese类也是一个抽象类,也是不能够进行实例化对象的,要使得 Chinese类有作用,我们必须派生出来另一个类,代码如下所示:

    class Guangximan : public Chinese 
    {
    	void driving(void) {cout<<"drive guangxi car"<<endl; }
    };
    

    这个时候,就可以用 Guangximan这个类来实例化对象了。

    多文件编程

    在前面的教程中,有一则教程说到了多文件编程,在 C++中也就是将类的声明放到头文件中,将类的实现放在.cpp文件中,为了更好地阐述这种方法,我们用实例来进行讲解,首先,来看一下,所涉及到地所有文件有哪些:

    image-20210222103409774

    可以看到上述有6个文件,我们首先来看 Chinese.h这个文件,代码如下所示:

    #ifndef _CHINESE_H
    #define _CHINESE_H
    
    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    
    using namespace std;
    
    class Chinese
    {
    public:
        void eating(void); 
        void wearing(void);
        void drivering(void);
    };
    
    #endif
    

    通过上述地.h文件可以看出,在这里的Chinese类中,它只涉及到类成员函数的一个声明,并没有成员函数的实现,我们继续来看Chinese.cpp的类实现:

    #include "Chinese.h"
    
    void Chinese::eating(void)
    {
        cout << "use chopsticks to eat" << endl;
    }
    
    void Chinese::wearing(void)
    {
        cout << "wear chinese style" << endl;
    }
    
    void Chinese::drivering(void)
    {
        cout << "driver china car" << endl;
    }
    

    按照上述这样一种方法,我们继续来实现Englishman类中的代码,首先是Englishman.h中的代码,代码如下所示:

    #ifndef _ENGLISHMAN_H
    #define _ENGLISHMAN_H
    
    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    
    using namespace std;
    
    class Englishman
    {
    public:
        void eating(void);
        void wearing(void);
        void driver(void);
    };
    
    #endif
    

    继续看.cpp中的代码,代码如下所示:

    #include "Englishman.h"
    
    void Englishman::eating(void)
    {
        cout << "use chopsticks to eat" << endl;
    }
    
    void Englishman::wearing(void)
    {
        cout << "wear chinese style" << endl;
    }
    
    void Englishman::drivering(void)
    {
        cout << "driver china car" << endl;
    }
    

    至此,除了主函数以外的代码就编写完了,我们继续来看主函数的代码:

    #include "Englishman.h"
    #include "Chinese.h"
    
    int main(int argc, char **argv)
    {
    	Englishman e;
    	Chinese c;
    
    	e.eating();
    	c.eating();
    	
    	return 0;
    }
    

    在前面的教程中,我们就说过,如果是多文件的话,需要编写 Makefile文件,Makefile文件代码如下:

    Human: main.o Chinese.o Englishman.o Human.o
    	g++ -o $@ $^
    
    %.o : %.cpp
    	g++ -c -o $@ $<
    
    clean:
    	rm -f *.o Human	
    

    上述代码就不再这里赘述了,跟之前教程中的 Makefile基本是一样的,有了Makefile之后,编译代码只需要使用 make命令就行了,编译结果如下所示:

    image-20210222105051169

    上述代码中,如果我们想要增添功能,比如说ChineseEnglishman都有名字,那么就可以增添设置名字和获取名字这两种方法,首先是 Chinese的代码,代码如下:

    #ifndef _CHINESE_H
    #define _CHINESE_H
    
    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    
    using namespace std;
    
    class Chinese{
    private:
    	char *name;
    public:
    	void setName(char *name);
    	char *getName(void);
    	void eating(void);
    	void wearing(void);
    	void driving(void);
    	~Chinese();
    };
    
    #endif
    

    然后是.cpp中的代码:

    #include "Chinese.h"
    
    void Chinese::setName(char *name) 
    {
    	this->name = name;
    }
    
    char *Chinese::getName(void) 
    {
    	return this->name;
    }
    
    /*其他成员函数实现同上,这里省略*/
    

    写完了 Chinese的代码,然后是Englishman中的代码,首先是Englishman.h中的代码:

    #ifndef _ENGLISHMAN_H
    #define _ENGLISHMAN_H
    
    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    
    using namespace std;
    
    class Englishman {
    private:
    	char *name;
    public:
    	void setName(char *name);
    	char *getName(void);
    	void eating(void);
    	void wearing(void);
    	void driving(void);
    	~Englishman();
    };
    
    #endif
    

    紧接着,是.cpp中的代码:

    #include "Englishman.h"
    
    void Englishman::setName(char *name) 
    {
    	this->name = name;
    }
    
    char *Englishman::getName(void) 
    {
    	return this->name;
    }
    

    以这样的方式增添功能,确实是可行的,但是我们假设一下,如果类很多,除了中国人和英国人还有很多个国家的人,如果这些类都要增加相同的功能,这个工作量就比较大了,那要如何解决这个问题呢?这个时候,我们就可以引入一个新类Human,然后,将每个类相同的部分写在这个类里面,其他类,诸如EnglisnmanChinese就可以从Human类中继承而来,那这个时候,增添的操作,就只需要在 Human类中增加就好了,不需要改动ChineseEnglishman,工作量就小了很多。我们来看 Human类的代码实现,首先是.h代码的实现:

    #ifndef _HUMAN_H
    #define _HUMAN_H
    
    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    
    using namespace std;
    
    class Human {
    private:
    	char *name;
    
    public:
    	void setName(char *name);
    	char *getName(void);	
    };
    
    #endif
    

    然后是.cpp代码的实现:

    #include "Human.h"
    
    void Human::setName(char *name) 
    {
    	this->name = name;
    }
    
    char *Human::getName(void) 
    {
    	return this->name;
    }
    

    有了 Human类之后,我们就可以来实现我们所说的 EnglishmanChinese类了,代码如下所示:

    #ifndef _ENGLISHMAN_H
    #define _ENGLISHMAN_H
    
    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    
    #include "Human.h"
    
    using namespace std;
    
    class Englishman : public Human 
    {
    public:
    	void eating(void);
    	void wearing(void);
    	void driving(void);
    	~Englishman();
    };
    
    #endif
    

    然后是Chinese的代码:

    #ifndef _CHINESE_H
    #define _CHINESE_H
    
    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    
    #include "Human.h"
    
    using namespace std;
    
    class Chinese : public Human
    {
    public:
    	void eating(void);
    	void wearing(void);
    	void driving(void);
    	~Chinese();
    };
    
    #endif
    

    可以看到 EnglishmanChinese都是继承自Human类,这个时候,就不需要再自己实现setNamegetName了。

    我们继续来完善我们的代码,先从主函数说起,主函数代码如下所示:

    void test_eating(Human *h)
    {
        h->eating();
    }
    
    int main(int argc, char **argv)
    {
        Englishman e;
        Chinese c;
        
        Human * h[2] = {&e,&h};
        int i;
        for (i = 0; i < 2; i++)
            test_eating(h[i]);
        
        return 0;
    }
    

    简要说明一下主函数代码的意思,其实就是定义了一个指针数组,然后遍历整个指针数组,一次将数组内的成员传入test_eating()函数内,根据传入的参数不同执行不同的eating函数,说到这里,实际上是跟前面一则教程中所将的抽象类和虚函数概念所结合起来的,因此,这里也是采用相同的思路,将 Human类设置为抽象类,然后其他类由Human类派生而来,下面就来看Human类的代码:

    #ifndef _HUMAN_H
    #define _HUMAN_H
    
    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    
    using namespace std;
    
    class Human {
    private:
    	char *name;
    
    public:
    	void setName(char *name);
    	char *getName(void);
    	virtual void eating(void) = 0;
    	virtual void wearing(void) = 0;
    	virtual void driving(void) = 0;	
    };
    
    #endif
    

    然后是Human.cpp的代码:

    #include "Human.h"
    
    void Human::setName(char *name) 
    {
    	this->name = name;
    }
    
    char *Human::getName(void) 
    {
    	return this->name;
    }
    

    实现了 Human类的代码之后,我们来看ChineseEnglishman的代码,代码如下所示,首先是 Englishman.h

    
    #ifndef _ENGLISHMAN_H
    #define _ENGLISHMAN_H
    
    #include <iostream>
    #include <string.h>
    #include <unistd.h>
    
    #include "Human.h"
    
    using namespace std;
    
    class Englishman : public Human 
    {
    public:
    	void eating(void);
    	void wearing(void);
    	void driving(void);
    };
    
    #endif
    

    紧接着是 Englishman.cpp的代码:

    #include "Englishman.h"
    
    void Englishman::eating(void) 
    { 
    	cout<<"use knife to eat"<<endl; 
    }
    
    void Englishman::wearing(void) 
    {
    	cout<<"wear english style"<<endl; 
    }
    
    void Englishman::driving(void) 
    {
    	cout<<"drive english car"<<endl; 
    }
    

    Chinese的代码就不展示了,和Englishman是一个道理,总的来说,上述实际上也就是本节教程中抽象类的一个多文件的实现。

    动态链接库

    回顾上述的代码中的 Makefile文件,代码如下所示:

    Human: main.o Chinese.o Englishman.o Human.o
    	g++ -o $@ $^
    
    %.o : %.cpp
    	g++ -c -o $@ $<
    
    clean:
    	rm -f *.o Human	
    

    通过第一行代码,我们知道只要更改main.cChinese.cEnglishman.c以及Human.o中的任意一个文件,都会导致重新生成一个 Human文件,考虑到这一点,实际上我们可以将 Chinese.oEnglishman.oHuman.o做成一个动态库,至于这么做的原因是因为我们在开发一个大的项目的时候,会涉及到一个程序由多个人编写,基本会分为两类,一个是应用编程,一个是类编程,那么这两者的区别如下所示:

    • 应用编程:使用类
    • 类编程:提供类编程,比如说 EnglishmanChinese

    基于此,我们将之前的程序更改为这种形式,分为应用编程类编程,基于上述我们对应用编程和类编程的区别的阐述,我们可以知道在刚刚那个程序,main.c就是应用编程,而Englishman.cChinese.cHuman.c就是类编程,而我们只需要更改 Makefile就可以实现这样一个功能,更改之后的Makefile如下所示:

    Human: main.o libHuman.so
    	g++ -o $@ $< -L./ -lHuman
    
    %.o : %.cpp
    	g++ -fPIC -c -o $@ $<
    
    libHuman.so : Englishman.o Chinese.o Human.o
    	g++ -shared -o $@ $^
    
    clean:
    	rm -f *.o Human	
    

    对比于之前的Makefile,我们可以看出第一行,Chinese.oEnglishman.oHuman.o替换成了现在的 libHuman.so,也就是说现在的 Human文件生成依赖于main.olibHuman.so这两个文件。第二行中的-L是表示,编译的时候,指定搜索库的路径,而整个路径就是紧随其后的./,表示的是当前文件夹下。而后面的-lHuman表示的是当前链接的是Human整个库,要完全理解这里,还需要了解下Linux下的.so文件。

    .so文件,被称之为共享库,是share object,用于动态链接,说到这里,可能会有所疑惑,明明写的是libHuman.so,为何在后面链接的时候写的是-lHuman,并不是-llibHuman呢,这就要了解一下.so文件的命名规则,.so文件是按照如下命名方式进行命名的:lib+函数库名+.so+版本号信息,也就是说虽然写的是libHuman.so,但是实际生成的共享库为Human,也就是为什么后面是-lHuman了。

    继续来看Makefile代码,可以看到第四行也与之前的代码不相同,多了一个 -fPIC,这个参数的作用是:生成位置无关目标码,用于生成动态链接库

    继续来看第八行,其中用到了一个之前未曾用过的-shared命令,这个命令的作用是:此选项将尽量使用动态库,为默认选项。优点:生成文件比较小。缺点:运行时需要系统提供动态库

    至此,Makefile代码就完了,那么更改了的代码与之前存在什么区别呢,我们先来回顾一下之前代码的主函数:

    #include "Human.h"
    #include "Englishman.h"
    #include "Chinese.h"
    
    void test_eating(Human *h)
    {
    	h->eating();
    }
    
    
    int main(int argc, char **argv)
    {
    	Englishman e;
    	Chinese c;
    
    	Human* h[2] = {&e, &c};
    	int i;
    	for (i = 0; i < 2; i++)
    		test_eating(h[i]);
    
    	return 0;
    }
    

    然后,我们进行编译,运行:

    image-20210223190725028

    在上述中,我们看到编译我们是用make命令进行编译的,然后在运行可执行代码的时候,我们采用的是LD_LIBRARY_PATH=./ ./Human,与前面的教程不同,这次在运行可执行文件的时候,多了LD_LIBRARY_PATH=./,这是因为现在使用了动态库,而这条多出来的语句是来指明动态库的路径的。

    最后,我们来测试一下,我们使用动态链接库所带来的优点,比如,我现在更改了Chinese.cppeating函数,代码如下:

    void Chinese::eating(void) 
    { 
    	cout<<"use chopsticks to eat,test"<<endl; 
    }
    

    然后,如果没有使用动态链接库,那么这个时候,如果要执行这个修改过的代码,就需要重新生成可执行文件,但是现在使用了动态链接库,也就是说,不需要重新生成可执行文件了,我们只需要重新生成动态链接库就好了,编译命令如下所示:
    image-20210223191802201
    可见,上述只重新生成了Human库文件,并没有重新生成可执行文件,代码运行正确,这样也就做到了应用编程和类编程分离。

    小结

    上述便是本期教程的所有内容,教程所涉及的代码可以通过百度云链接的方式获取。

    链接:https://pan.baidu.com/s/1fB78jG6PdMNcXMfPwtqyvw
    提取码:cquv

    如果我的文章对您有所帮助,欢迎关注我的个人公众号:wenzi嵌入式软件
    在这里插入图片描述

    展开全文
  • 一、选择题 1.下列关于对象初始化的叙述中,正确的是 ...2.下列关于基类和派生类关系的叙述中,正确的是  A 每个类最多只能有一个直接基类  B 派生类中的成员可以访问基类中的任何成员  C 基
  • PTA 继承第一次实验(选择题解析)

    千次阅读 2019-06-04 14:29:39
    继承第一次实验 2-1 一个类的私有成员 (2分) 只能被该类的成员函数访问 ...以下关于C++语言中继承的叙述中,错误的是( )。 (2分) 继承是父类和子类之间共享数据和方法的机制 继承定义了一种类与类之间的关系...
  • 一、选择题 1.下列关于对象初始化的叙述中,正确的是  A 定义对象的时候不能对对象进行初始化  B 定义对象之后可以显式地调用...2.下列关于基类和派生类关系的叙述中,正确的是  A 每个类最多只能有一个直接基类
  • 关于工厂方法,以下叙述不正确是(C) A、Factory Method 是一个用于帮助分配创建责任模式。 B、Factory Method 模式和 Abstract Factory 模式总是配合使用 C、Factory Method 适用于将对象实例化规则推迟到...
  • 下面是关于ios类的叙述,正确的是____________。 它是istream类和ostream类的虚基类 它只是istream类的虚基类 它只是ostream类的虚基类 它是iostream类的虚基类 下列程序中声明一个园类circle和一个桌子类table。...
  • flash shiti

    2014-03-14 10:32:41
    12. 如果导入是图像序列中一个picture001.bmp 文件,并且序列中其他文件位于相同 文件夹中,则将被识别为图像序列将是下面哪些: □ A. picture001.bmp □ B. picture002.bmp □ C. picture003.bmp □ D. ...
  • 1. 下列关于对象初始化的叙述中,正确的是( )  A.定义对象的时候不能对对象进行初始化  B.定义对象之后可以显式地调用构造函数进行初始化  C.定义对象时将自动调用构造函数进行初始化  D.在一个类中必须...
  • c++ 程序设计

    2019-01-20 22:53:37
    11.1 继承与派生的概念 11.2 派生类的声明方式 11.3 派生类的构成 11.4 派生类成员的访问属性 11.4.1 公用继承 11.4.2 私有继承 11.4.3 保护成员和保护继承 11.4.4 多级派生时的访问属性 11.5 派生类的构造函数和...
  • 面向对象与C++试题.doc

    2013-12-16 20:49:17
    12、在公用继承方式下,有关派生类对象和基类对象的关系,不正确的叙述是( )。 A.派生类的对象可以赋给基类的对象 B.派生类的对象可以初始化基类的引用 C.派生类的对象可以直接访问基类中的成员 D.派生类的...
  • C#与.NET3.5高级程序设计第四版高清PDF中文完整版

    千次下载 热门讨论 2011-07-05 10:25:50
    本书是c#领域久负盛名经典著作,深入全面地叙述了c#编程语言和.net平台核心,并以大量示例剖析相关概念。书中介绍了c#各种语言构造、.net 2.0类、核心api、公共中间语言(cil)、动态程序集和asp.net扩展等...
  • C++复习资料之系列

    2008-12-30 21:35:45
    (c) *p表示变量i值 (d) p值是变量i地址 15.执行语句 int a = 5,b = 10,c;int *p1 = &a, *p2 = &b; 后,下面不正确赋值语句是( b )。 (a) *p2 = b; (b) p1 = a; (c) p2 = p1; (d) c = *p1 *...
  • c++程序设计 谭浩强part1

    热门讨论 2010-02-23 20:07:09
    本书作者花费了很大精力去考虑怎样使读者易于接受和理解,尽量用通俗易懂方法和语言叙述复杂概念,力求减少初学者学习C++困难。书中所举程序,是从教学角度考虑,非常切合教学之用。 (4)体现C++标准...
  • 本书作者花费了很大精力去考虑怎样使读者易于接受和理解,尽量用通俗易懂方法和语言叙述复杂概念,力求减少初学者学习C++困难。书中所举程序,是从教学角度考虑,非常切合教学之用。 (4)体现C++标准...
  • 本书作者花费了很大精力去考虑怎样使读者易于接受和理解,尽量用通俗易懂方法和语言叙述复杂概念,力求减少初学者学习C++困难。书中所举程序,是从教学角度考虑,非常切合教学之用。 (4)体现C++标准...
  • 本书是C#领域久负盛名经典著作,深入全面地叙述了C#编程语言和.NET平台核心,并以大量示例剖析相关概念。书中介绍了C#各种语言构造、.NET 2.0类、核心API、公共中间语言(CIL)、动态程序集和ASP.NET扩展等...
  • 本书是C#领域久负盛名经典著作,深入全面地叙述了C#编程语言和.NET平台核心,并以大量示例剖析相关概念。书中介绍了C#各种语言构造、.NET 2.0类、核心API、公共中间语言(CIL)、动态程序集和ASP.NET扩展等...
  • 本书是C#领域久负盛名经典著作,深入全面地叙述了C#编程语言和.NET平台核心,并以大量示例剖析相关概念。书中介绍了C#各种语言构造、.NET 2.0类、核心API、公共中间语言(CIL)、动态程序集和ASP.NET扩展等...
  • 本书是C#领域久负盛名经典著作,深入全面地叙述了C#编程语言和.NET平台核心,并以大量示例剖析相关概念。书中介绍了C#各种语言构造、.NET 2.0类、核心API、公共中间语言(CIL)、动态程序集和ASP.NET扩展等...
  • 9.3.1 派生类中构造函数操作 448 9.3.2 声明类保护成员 452 9.3.3 继承类成员访问级别 455 9.4 派生类中复制构造函数 456 9.5 友元类成员 461 9.5.1 友元类 462 9.5.2 对类友元关系限制 462...
  • 14.2.1 派生的异常 319 14.2.2 多个异常的组合 321 14.3 捕捉异常 321 14.3.1 重新抛出 322 14.3.2 捕捉所有异常 322 14.4 资源管理 324 14.4.1 构造函数和析构函数的使用 325 14.4.2 auto_ptr 326 14.4.3 ...
  • 14.2.1 派生的异常 319 14.2.2 多个异常的组合 321 14.3 捕捉异常 321 14.3.1 重新抛出 322 14.3.2 捕捉所有异常 322 14.4 资源管理 324 14.4.1 构造函数和析构函数的使用 325 14.4.2 auto_ptr 326 14.4.3 ...
  • C++程序设计语言(特别版)--源代码

    热门讨论 2012-04-23 07:33:51
    14.2.1 派生的异常 319 14.2.2 多个异常的组合 321 14.3 捕捉异常 321 14.3.1 重新抛出 322 14.3.2 捕捉所有异常 322 14.4 资源管理 324 14.4.1 构造函数和析构函数的使用 325 14.4.2 auto_ptr 326 14.4.3 ...
  • CruiseYoung提供带有详细书签电子书籍目录 http://blog.csdn.net/fksec/article/details/7888251 该资料是《Visual C++ 2010入门经典(第5版)》源代码及课后练习答案 对应书籍资料见: Visual C++ 2010...
  • asp.net面试题

    2011-05-27 17:56:26
    四、关于ASP.NET中代码隐藏文件描述正确是( )。 A.Web窗体页程序逻辑由代码组成,这些代码创建用于与窗体交互。该文件称作为“代码隐藏”文件,如果用C#创建,该文件将具有“.ascx.cs”扩展名 B.web...
  • 多视点方法也是管理需求变化一种新方法,它可以用于管理不一致性, 并进行关于变化推理。 2. M公司软件产品以开发实验型新软件为主。用瀑布模型进行软件开发已经有近十年了,并取得了一些成功。若你作为一...

空空如也

空空如也

1 2
收藏数 30
精华内容 12
关键字:

关于派生的叙述