07C++继承与内存模型分析

继承

1.继承

简单理解:子类从父类继承成员变量和成员函数

父类中所有非静态成员属性都会被子类继承下去,和java一致

父类中的private私有成员,是被编译器隐藏了,但是被继承了。

#include <iostream>
using namespace std;

class People
{
private:
    string name;
    int age;

public:
    void setName(string name);
    void setAge(int age);
    string getName() const;
    int getAge() const;
};

void People::setName(string name)
{
    this->name = name;
}

void People::setAge(int age)
{
    this->age = age;
}

string People::getName() const
{
    return this->name;
}

int People::getAge() const
{
    return this->age;
}

class Student : public People
{
public:
    void setScore(int score);
    int getScore() const;

private:
    int score;
};

void Student::setScore(int score)
{
    this->score = score;
}

int Student::getScore() const
{
    return this->score;
}

void test()
{
    Student stu;
    stu.setScore(90);
    stu.setName("zhangsan");
    stu.setAge(20);
    cout << stu.getName() << stu.getAge() << stu.getScore() << endl;
}

int main(int argc, char const *argv[])
{
    test();
    system("pause");
    return 0;
}

2.三种继承方式,public/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继承方式
publicpublic-> protected-> private
protectedprotectedprotected-> private
private-> 不可访问-> 不可访问

在派生类中访问基类 private 成员的唯一方法就是借助基类的非 private 成员函数,如果基类没有非 private 成员函数,那么该成员在派生类中将无法访问。

3.通过using改变访问的权限

可以改变基类成员在子类中的访问权限,将 public 改为 private、将 protected 改为 public。

注意:private不能修改,private修饰的基类在子类中无法使用和查询到。

#include <iostream>
using namespace std;

class People
{
protected:
    string name;
    int age;

public:
    void show() const;
    void learning() const;
};

void People::show() const
{
    cout << this->name << this->age << endl;
}

void People::learning() const
{
    cout << this->name << this->age << endl;
}

class Student : public People
{
public:
    using People::age;  //将protected改为public
    using People::name; //将protected改为public
    float score;

private:
    using People::show; //将public改为private

public:
    void learning() const;
    void show() const;
};

void Student::learning() const
{
    cout << this->name << this->age << this->score << endl;
}

void test()
{
    Student stu;
    stu.name = "zhangsan";
    stu.age = 20;
    stu.score = 30.0f;
    // stu.show(); //错误,权限调整为private
    //使用的是派生类新增的成员函数,而不是从基类继承的
    stu.learning();
    //使用的是从基类继承来的成员函数
    stu.People::learning();
}

int main(int argc, char const *argv[])
{
    test();
    system("pause");
    return 0;
}

4.子类成员与基类成员重名

与java一致,默认会使用自己的成员,而不是基类的成员。

    //使用的是派生类新增的成员函数,而不是从基类继承的
    stu.learning();
    //使用的是从基类继承来的成员函数
    stu.People::learning();//需要加作用域

注意:基类成员函数和派生类成员函数不构成重载。 同名就会被覆盖,默认执行自己的成员

5.类继承时的作用域嵌套

1.子类是嵌套在父类中的,首先会在自己的类中查找,如果找到,结束,如果找不到,则去父类中查找,

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class A{
public:
	virtual void funA(){
		cout<<"A"<<endl;
	}
};
class B {
public:
	virtual void funA(int a){
		cout<<"B"<<endl;
	}
};

class C: public A,public B{
	
};
int main() {
	C c;
	c.funA(7);//报错,不知道找A还是B
}
int main(){
	C c;
	A *a = &c;
	a->funA();//正确,找A
}

a的静态类型对应的类(也就是类A)的作用域中查找funA

6.构造函数和析构顺序

#include <iostream>
using namespace std;

class Base
{
    public:
        Base();
        ~Base();
};
Base::Base()
{
    cout << "Base创建了" << endl;
}

Base::~Base()
{
     cout << "Base被析构了" << endl;   
}

class A :public Base
{
    public:
        A();
        ~A();
};
A::A()
{
    cout << "A创建了" << endl;
}

A::~A()
{
    cout << "A被析构了" << endl;  
}

void test()
{
    A a;
}

int main(int argc, char const *argv[])
{
    test();
    system("pause");
    return 0;
}

Base创建了
A创建了
A被析构了
Base被析构了
请按任意键继续. . .

总结:与类中其他类对象一致,都是其他类对象先创建,本身再创建, 析构时,是本身类先析构,其他类对象后析构。

​ 另外注意,创建A对象时,其实父类也会被创建,不然怎么析构了。

7.同名静态重名

#include <iostream>
using namespace std;

class Base
{
public:
    static int age;
    static void func()
    {
    }

    static void func(int num)
    {
    }
};

int Base::age = 200;

class A : public Base
{
public:
    static int age;
    static void func()
    {
    }
};
int A::age = 10;

void test()
{
    //类对象
    A a;
    a.age;
    a.Base::age;
    //类名
    A::age;
    Base::age;
    A::Base::age;


    //类静态函数重名
    a.func();
    a.Base::func();
    A::func();
    A::Base::func();
    Base::func(20);
}

int main(int argc, char const *argv[])
{
    test();
    system("pause");
    return 0;
}

8.多继承语法

容易产生二义性。不仅仅成员会出现二义性,而且子类会出现二义性,是骡子还是马就搞不清楚了。

1.成员二义性
#include <iostream>
using namespace std;

class Tiger
{
public:
    static int age;
};

int Tiger::age = 20;

class lion
{
public:
    static int age;
};
int lion::age = 30;


class Dog :public Tiger,public lion
{
};


void test()
{
    Dog d;
    // d.age;//会产生二义性,不清楚导是Tiger还是lion的
    d.Tiger::age;
    d.lion::age;
}

int main(int argc, char const *argv[])
{
    system("pause");
    return 0;
}

2.类二义性,特别是菱形继承

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WG1J9ALD-1625408289973)(G:\markdown\C++\C++基础\草泥马.png)]

这就是所谓的菱形继承,动物有年龄,骡子也有,马也有,当马修改了年龄,则骡子的年龄修不修改。

#include <iostream>
using namespace std;

class Animal
{
public:
     int age;
};

class Mule : public Animal
{
};

class horse : public Animal
{
};

class GrassHorse : public Mule, public horse
{
};

void test()
{
    GrassHorse g;
    g.Mule::age = 15;
    g.horse::age = 30;

    cout << g.Mule::age << endl;
    cout << g.horse ::age << endl;
}

int main(int argc, char const *argv[])
{
    test();
    system("pause");
    return 0;
}

15
30

为什么会出现这种情况,从构造函数和析构顺序,我们知道,子类是会继承父类所有成员,所以本质上骡子保存了一份Animal的年龄,horse也保存了一份Animal的年龄,再修改骡子的年龄时,马的年龄并不会因此改变。

3.如何解决多继承带来地是菱形二义性问题
class Mule : virtual public Animal
{
};

class horse : virtual public Animal
{
};

骡子和马都加上virtual虚继承,

为什么添加virtual虚继承之后,就不会出现问题了。

直接给出结论: Mule加上了virtual,那么Mule只会保存一份vbptr地指针,该指针指向了父类,同样,horse也是如此, 二者修改数据时,其实修改地是它们父类Animal地年龄,此时不会产生二义性。

总结:再开发中,尽量不要使用多继承,再java/go等面向对象开发语言中,多继承是被抛弃地概念。

9.借助指针突破访问权限private的限制

通过上面继承的讲解,我们知道子类会继承父类所有成员,即使private也会继承,子类是嵌入到父类中,创建子类,其实本质父类也会创建。父类private是被编译器隐藏的,导致子类无法访问,那么有没有办法突破private的限制。

答案是有的,还记得前面说过,如何计算一个类的字节大小,这里不在介绍了。我们根据字节的计算,来知道各个属性的位置。然后通过修改该段内存空间调整值。

++x有的编译器直接指向下一个成员,有的不是。跟编译器有关

#include <iostream>
using namespace std;

class A
{
private:
    int a;
    int b;
    char e;
    int c;

public:
    A(int a, int b, char e, int c);
    int getA() const;
};

A::A(int a, int b, char e, int c)
{
    this->a = a;
    this->b = b;
    this->e = e;
    this->c = c;
}

int A::getA() const
{
    return this->a;
}

void test()
{
    A d(10, 20, 'a', 30);
    int *x = (int *)&d; //指向d首地址
    cout << *x << endl;
    *x = 100;
    cout << d.getA() << endl;
    ++x;//自动指向像一个成岩
    int *y = x;
    cout << *y << endl;
    char *z = (char *)++x;
    cout << *z << endl;
    ++x;
    int *w = x;
    cout << *w << endl;
}

int main(int argc, char const *argv[])
{
    test();
    system("pause");
    return 0;
}

10.虚基类必须派生类初始化

规定:C++干脆规定必须由最终的派生类 D 来初始化虚基类 A,直接派生类 B 和 C 对 A 的构造函数的调用是无效的。避免产生二义性.

构造函数执行顺序:虚继承时构造函数的执行顺序与普通继承时不同:在最终派生类的构造函数调用列表中,不管各个构造函数出现的顺序如何,编译器总是先调用虚基类的构造函数,再按照出现的顺序调用其他的构造函数;而对于普通继承,就是按照构造函数出现的顺序依次调用的。

总结:虚基类必须有派生类初始化,而且必须第一个初始化,不按照构造顺序.

#include <iostream>
using namespace std;
//虚基类A
class A{
public:
    A(int a);
protected:
    int m_a;
};
A::A(int a): m_a(a){ }
//直接派生类B
class B: virtual public A{
public:
    B(int a, int b);
public:
    void display();
protected:
    int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
    cout<<"m_a="<<m_a<<", m_b="<<m_b<<endl;
}
//直接派生类C
class C: virtual public A{
public:
    C(int a, int c);
public:
    void display();
protected:
    int m_c;
};
C::C(int a, int c): A(a), m_c(c){ }
void C::display(){
    cout<<"m_a="<<m_a<<", m_c="<<m_c<<endl;
}
//间接派生类D
class D: public B, public C{
public:
    D(int a, int b, int c, int d);
public:
    void display();
private:
    int m_d;
};
D::D(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){ }
void D::display(){
    cout<<"m_a="<<m_a<<", m_b="<<m_b<<", m_c="<<m_c<<", m_d="<<m_d<<endl;
}
int main(){
    B b(10, 20);
    b.display();
   
    C c(30, 40);
    c.display();
    D d(50, 60, 70, 80);
    d.display();
    return 0;
}

D:😄(int a, int b, int c, int d): A(a), B(90, b), C(100, c), m_d(d){ } // A(a)必须要构建

11.派生类赋值给基类

1.案例
#include <iostream>
using namespace std;

class Base
{
public:
    int a;

public:
    Base(int a);
    void display();
};

void Base::display()
{
    cout << this->a << endl;
}

Base::Base(int a)
{
    this->a = a;
}

class B : public Base
{
public:
    int b;

public:
    B(int a, int b);
    void display();
};

B::B(int a, int b) : Base(a), b(b)
{
}

void B::display()
{
    cout << this->a << this->b << endl;
}

void test()
{
    Base base(10);
    B b(20, 30);
    base.display();
    b.display();

    base = b;//赋值,

    base.display();//执行的还是Base类,修改了Base类的值

    b.display();
}

int main(int argc, char const *argv[])
{
    test();
    system("pause");
    return 0;
}

10
2030
20
2030
请按任意键继续. . .

2.本质分析

**赋值的本质:**赋值的本质是将现有的数据写入已分配好的内存空间中,对象的内存只包含了成员,不包含成员函数(所有函数只有一份,可以前面的章节详细了解),所以不存在成员函数赋值问题.

Base:内存空间四字节;

B:内存空间8字节.

base = b;// 该过程,是将B的8字节内存空间数值写入到Base四字节空间,显然是装不下的.

此时,编译时会自动将B的基类成员进行赋值,舍弃B的成员.所以最后只是将B的基类成员写入到Base的内存空间

注意:这种舍弃,是不可逆的.

总结:派生类不能为赋值给基类,否则派生类会舍弃自身的成员, 基类可以赋值给派生类.

12.将派生类指针赋值给基类指针时到底发生了什么?

案例:

#include <iostream>
using namespace std;

class Base
{
public:
    int a;

public:
    Base(int a);
    void display();
};

void Base::display()
{
    cout << this->a << endl;
}

Base::Base(int a)
{
    this->a = a;
}

class B : public Base
{
public:
    int b;

public:
    B(int a, int b);
    void display();
};

B::B(int a, int b) : Base(a), b(b)
{
}

void B::display()
{
    cout << this->a << this->b << endl;
}

void test()
{
    Base *base = new Base(10);
    B *b = new B(20, 30);

    base->display();//10
    b->display();//2030

    base = b;//base指针指向b

    base->display();//20,原来是10
    cout << base->a << endl;//20,值修改了
}

int main(int argc, char const *argv[])
{
    test();
    system("pause");
    return 0;
}

10
2030
20
20
发现:通过上面,我们发现base->display()调用的Base的成员函数,而base->a却是使用的B b的成员,为什么会发生这种现象.

结论:

  • 1.指针base = b;与对象赋值不同,指针的赋值,是指针改变了方向,指向另外一个内存空间.base指针指向b的内存空间. 隐式指针 this 发生了变化
  • 2.编译器是通过类型来访问成员函数的,而不是所指的具体对象.
  • 3.成员函数,漏掉了一点,访问的成员函数属于父类,则首先调用父类的成员,反之,成员函数属于子类,则首先调用子类的成员。(多态时,解答该问题)。

所以,base指针指向b的内存空间,可以访问b的成员,但是base的类型没有变化,编译器依旧会让器指向Base类型的成员函数.

se(a), b(b)
{
}

void B::display()
{
cout << this->a << this->b << endl;
}

void test()
{
Base *base = new Base(10);
B *b = new B(20, 30);

base->display();//10
b->display();//2030

base = b;//base指针指向b

base->display();//20,原来是10
cout << base->a << endl;//20,值修改了

}

int main(int argc, char const *argv[])
{
test();
system(“pause”);
return 0;
}


10
2030
20
20
**发现:通过上面,我们发现base->display()调用的Base的成员函数,而base->a却是使用的B b的成员,为什么会发生这种现象.**

结论:

- 1.指针base = b;与对象赋值不同,指针的赋值,是指针改变了方向,指向另外一个内存空间.base指针指向b的内存空间.  隐式指针 this 发生了变化
- 2.编译器是通过类型来访问成员函数的,而不是所指的具体对象.
- 3.成员函数,漏掉了一点,访问的成员函数属于父类,则首先调用父类的成员,反之,成员函数属于子类,则首先调用子类的成员。(多态时,解答该问题)。

所以,base指针指向b的内存空间,可以访问b的成员,但是base的类型没有变化,编译器依旧会让器指向Base类型的成员函数.



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
下面是一个完整的微信授权页面的示例,使用 Less 和 HTML 编写: HTML 代码: ``` <!DOCTYPE html> <html> <head> <title>微信授权页面</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> <link rel="stylesheet/less" type="text/css" href="style.less"> <script src="https://cdn.bootcdn.net/ajax/libs/less.js/3.12.2/less.min.js"></script> </head> <body> <div class="container"> <h1 class="title">微信授权页面</h1> <form class="form"> <input type="text" class="input" placeholder="请输入微信号或手机号"> <input type="password" class="input" placeholder="请输入密码"> <button class="button">登录</button> </form> </div> </body> </html> ``` Less 代码: ``` @primary-color: #07c160; @font-size: 14px; @border-radius: 4px; .container { padding: 20px; background-color: #f8f8f8; .title { font-size: 18px; color: @primary-color; margin-bottom: 10px; } .form { background-color: #fff; border-radius: @border-radius; padding: 20px; .input { border: none; border-bottom: 1px solid #ddd; height: 40px; line-height: 40px; font-size: @font-size; width: 100%; margin-bottom: 10px; &:last-child { margin-bottom: 20px; } } .button { background-color: @primary-color; color: #fff; border-radius: @border-radius; height: 40px; line-height: 40px; text-align: center; font-size: @font-size; } } } ``` 编译后的 CSS 代码: ``` .container { padding: 20px; background-color: #f8f8f8; } .container .title { font-size: 18px; color: #07c160; margin-bottom: 10px; } .container .form { background-color: #fff; border-radius: 4px; padding: 20px; } .container .form .input { border: none; border-bottom: 1px solid #ddd; height: 40px; line-height: 40px; font-size: 14px; width: 100%; margin-bottom: 10px; } .container .form .input:last-child { margin-bottom: 20px; } .container .form .button { background-color: #07c160; color: #fff; border-radius: 4px; height: 40px; line-height: 40px; text-align: center; font-size: 14px; } ``` 可以看到,使用 Less 和 HTML 编写微信授权页面,可以让代码更加简洁易懂,提高开发效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值