【学习QT必备的C++基础】C+(1),2024年最新二本学渣考研失败

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Golang全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注go)
img

正文

float Student::getscore(){ return m_score; }
int main(){
Student stu;
stu.setname(“小明”);
stu.setage(16);
stu.setscore(95.5f);
cout<<stu.getname()<<"的年龄是 “<<stu.getage()<<”,成绩是 "<<stu.getscore()<<endl;
return 0;
}

运行结果:
小明的年龄是 16,成绩是 95.5

本例中,People 是基类,Student 是派生类。Student 类继承了 People 类的成员,同时还新增了自己的成员变量 score 和成员函数 setscore()、getscore()。这些继承过来的成员,可以通过子类对象访问,就像自己的一样。

请认真观察代码第21行:

class Student: public People

这就是声明派生类的语法。class 后面的“Student”是新声明的派生类,冒号后面的“People”是已经存在的基类。在“People”之前有一关键宇 public,用来表示是公有继承。

由此总结出继承的一般语法为:

class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};

继承方式包括 public(公有的)、private(私有的)和 protected(受保护的),此项是可选的,如果不写,那么默认为 private。我们将在下节详细讲解这些不同的继承方式。

C++三种继承方式

已剪辑自: http://c.biancheng.net/view/2269.html

C++继承的一般语法为:

class 派生类名:[继承方式] 基类名{
派生类新增加的成员
};

继承方式限定了基类成员在派生类中的访问权限,包括 public(公有的)、private(私有的)和 protected(受保护的)。此项是可选项,如果不写,默认为 private(成员变量和成员函数默认也是 private)。

现在我们知道,public、protected、private 三个关键字除了可以修饰类的成员,还可以指定继承方式。

public、protected、private 修饰类的成员

类成员的访问权限由高到低依次为 public --> protected --> private,我们在《C++类成员的访问权限以及类的封装》一节中讲解了 public 和 private:public 成员可以通过对象来访问,private 成员不能通过对象访问。

现在再来补充一下 protected。protected 成员和 private 成员类似,也不能通过对象访问。但是当存在继承关系时,protected 和 private 就不一样了:基类中的 protected 成员可以在派生类中使用,而基类中的 private 成员不能在派生类中使用,下面是详细讲解。

public、protected、private 指定继承方式

不同的继承方式会影响基类成员在派生类中的访问权限。

1) public继承方式

  • 基类中所有 public 成员在派生类中为 public 属性;
  • 基类中所有 protected 成员在派生类中为 protected 属性;
  • 基类中所有 private 成员在派生类中不能使用。

2) protected继承方式

  • 基类中的所有 public 成员在派生类中为 protected 属性;
  • 基类中的所有 protected 成员在派生类中为 protected 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

3) private继承方式

  • 基类中的所有 public 成员在派生类中均为 private 属性;
  • 基类中的所有 protected 成员在派生类中均为 private 属性;
  • 基类中的所有 private 成员在派生类中不能使用。

通过上面的分析可以发现:

  1. 基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为 protected 时,那么基类成员在派生类中的访问权限最高也为 protected,高于 protected 的会降级为 protected,但低于 protected 不会升级。再如,当继承方式为 public 时,那么基类成员在派生类中的访问权限将保持不变。

也就是说,继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问权限的。

  1. 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。
  2. 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public 或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
  3. 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 protected。

注意,我们这里说的是基类的 private 成员不能在派生类中使用,并没有说基类的 private 成员不能被继承。实际上,基类的 private 成员是能够被继承的,并且(成员变量)会占用派生类对象的内存,它只是在派生类中不可见,导致无法使用罢了。private 成员的这种特性,能够很好的对派生类隐藏基类的实现,以体现面向对象的封装性。

下表汇总了不同继承方式对不同属性的成员的影响结果 继承方式/基类成员 public成员 protected成员 private成员 public继承 protected继承 private继承

publicprotected不可见
protectedprotected不可见
privateprivate不可见

由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所以实际开发中我们一般使用 public。

【示例】演示类的继承关系。

#include
using namespace std;
//基类People
class People{
public:
void setname(char *name);
void setage(int age);
void sethobby(char *hobby);
char *gethobby();
protected:
char *m_name;
int m_age;
private:
char *m_hobby;
};
void People::setname(char *name){ m_name = name; }
void People::setage(int age){ m_age = age; }
void People::sethobby(char *hobby){ m_hobby = hobby; }
char *People::gethobby(){ return m_hobby; }
//派生类Student
class Student: public People{
public:
void setscore(float score);
protected:
float m_score;
};
void Student::setscore(float score){ m_score = score; }
//派生类Pupil
class Pupil: public Student{
public:
void setranking(int ranking);
void display();
private:
int m_ranking;
};
void Pupil::setranking(int ranking){ m_ranking = ranking; }
void Pupil::display(){
cout<<m_name<<“的年龄是”<<m_age<<“,考试成绩为”<<m_score<<“分,班级排名第”<<m_ranking<<“,TA喜欢”<<gethobby()<<“。”<<endl;
}
int main(){
Pupil pup;
pup.setname(“小明”);
pup.setage(15);
pup.setscore(92.5f);
pup.setranking(4);
pup.sethobby(“乒乓球”);
pup.display();
return 0;
}

运行结果:
小明的年龄是15,考试成绩为92.5分,班级排名第4,TA喜欢乒乓球。

这是一个多级继承的例子,Student 继承自 People,Pupil 又继承自 Student,它们的继承关系为 People --> Student --> Pupil。Pupil 是最终的派生类,它拥有基类的 m_name、m_age、m_score、m_hobby 成员变量以及 setname()、setage()、sethobby()、gethobby()、setscore() 成员函数。

注意,在派生类 Pupil 的成员函数 display() 中,我们借助基类的 public 成员函数 gethobby() 来访问基类的 private 成员变量 m_hobby,因为 m_hobby 是 private 属性的,在派生类中不可见,所以只能借助基类的 public 成员函数 sethobby()、gethobby() 来访问。

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

改变访问权限

使用 using 关键字可以改变基类成员在派生类中的访问权限,例如将 public 改为 private、将 protected 改为 public。

注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问权限,因为基类中 private 成员在派生类中是不可见的,根本不能使用,所以基类中的 private 成员在派生类中无论如何都不能访问。

using 关键字使用示例:

#include
using namespace std;
//基类People
class People {
public:
void show();
protected:
char *m_name;
int m_age;
};
void People::show() {
cout << m_name << “的年龄是” << m_age << endl;
}
//派生类Student
class Student : public People {
public:
void learning();
public:
using People::m_name; //将protected改为public
using People::m_age; //将protected改为public
float m_score;
private:
using People::show; //将public改为private
};
void Student::learning() {
cout << “我是” << m_name << “,今年” << m_age << “岁,这次考了” << m_score << “分!” << endl;
}
int main() {
Student stu;
stu.m_name = “小明”;
stu.m_age = 16;
stu.m_score = 99.5f;
stu.show(); //compile error
stu.learning();
return 0;
}

代码中首先定义了基类 People,它包含两个 protected 属性的成员变量和一个 public 属性的成员函数。定义 Student 类时采用 public 继承方式,People 类中的成员在 Student 类中的访问权限默认是不变的。

不过,我们使用 using 改变了它们的默认访问权限,如代码第 21~25 行所示,将 show() 函数修改为 private 属性的,是降低访问权限,将 name、age 变量修改为 public 属性的,是提高访问权限。

因为 show() 函数是 private 属性的,所以代码第 36 行会报错。把该行注释掉,程序输出结果为:
我是小明,今年16岁,这次考了99.5分!

C++继承时的名字遮蔽问题

已剪辑自: http://c.biancheng.net/view/2271.html

如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。

下面是一个成员函数的名字遮蔽的例子:

#include
using namespace std;
//基类People
class People{
public:
void show();
protected:
char *m_name;
int m_age;
};
void People::show(){
cout<<“嗨,大家好,我叫”<<m_name<<“,今年”<<m_age<<“岁”<<endl;
}
//派生类Student
class Student: public People{
public:
Student(char *name, int age, float score);
public:
void show(); //遮蔽基类的show()
private:
float m_score;
};
Student::Student(char *name, int age, float score){
m_name = name;
m_age = age;
m_score = score;
}
void Student::show(){
cout<<m_name<<“的年龄是”<<m_age<<“,成绩是”<<m_score<<endl;
}
int main(){
Student stu(“小明”, 16, 90.5);
//使用的是派生类新增的成员函数,而不是从基类继承的
stu.show();
//使用的是从基类继承来的成员函数
stu.People::show();
return 0;
}

运行结果:
小明的年龄是16,成绩是90.5
嗨,大家好,我叫小明,今年16岁

本例中,基类 People 和派生类 Student 都定义了成员函数 show(),它们的名字一样,会造成遮蔽。第 37 行代码中,stu 是 Student 类的对象,默认使用 Student 类的 show() 函数。

但是,基类 People 中的 show() 函数仍然可以访问,不过要加上类名和域解析符,如第 39 行代码所示。

基类成员函数和派生类成员函数不构成重载

基类成员和派生类成员的名字一样时会造成遮蔽,这句话对于成员变量很好理解,对于成员函数要引起注意,不管函数的参数如何,只要名字一样就会造成遮蔽。换句话说,基类成员函数和派生类成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数,不管它们的参数是否一样。

下面的例子很好的说明了这一点:

#include
using namespace std;
//基类Base
class Base{
public:
void func();
void func(int);
};
void Base::func(){ cout<<“Base::func()”<<endl; }
void Base::func(int a){ cout<<“Base::func(int)”<<endl; }
//派生类Derived
class Derived: public Base{
public:
void func(char *);
void func(bool);
};
void Derived::func(char *str){ cout<<“Derived::func(char *)”<<endl; }
void Derived::func(bool is){ cout<<“Derived::func(bool)”<<endl; }
int main(){
Derived d;
d.func(“c.biancheng.net”);
d.func(true);
d.func(); //compile error
d.func(10); //compile error
d.Base::func();
d.Base::func(100);
return 0;
}

本例中,Base 类的func()func(int)和 Derived 类的func(char *)func(bool)四个成员函数的名字相同,参数列表不同,它们看似构成了重载,能够通过对象 d 访问所有的函数,实则不然,Derive 类的 func 遮蔽了 Base 类的 func,导致第 26、27 行代码没有匹配的函数,所以调用失败。

如果说有重载关系,那么也是 Base 类的两个 func 构成重载,而 Derive 类的两个 func 构成另外的重载。

至于 Base 类和 Derived 类的 4 个 func 为什么不会构成重载,我们将在VIP教程《C++类继承时的作用域嵌套,破解C++继承的一切秘密!》一节中讲解,届时读者必将有所顿悟。

C++类继承时的作用域嵌套,破解C++继承的一切秘密!

已剪辑自: http://c.biancheng.net/view/vip_2272.html

类其实也是一种作用域,每个类都会定义它自己的作用域,在这个作用域内我们再定义类的成员,这一点已在《类其实也是一种作用域》中讲到。当存在继承关系时,派生类的作用域嵌套在基类的作用域之内,如果一个名字在派生类的作用域内无法找到,编译器会继续到外层的基类作用域中查找该名字的定义。

换句话说,作用域能够彼此包含,被包含(或者说被嵌套)的作用域称为内层作用域(inner scope),包含着别的作用域的作用域称为外层作用域(outer scope)。一旦在外层作用域中声明(或者定义)了某个名字,那么它所嵌套着的所有内层作用域中都能访问这个名字。同时,允许在内层作用域中重新定义外层作用域中已有的名字。

假设 Base 是基类,Derived 是派生类,那么它们的作用域的嵌套关系如下图所示:

img

派生类的作用域位于基类作用域之内这一事实可能有点出人意料,毕竟在我们的代码中派生类和基类的定义是相互分离的。不过也恰恰因为类作用域有这种继承嵌套的关系,所以派生类才能像使用自己的成员一样来使用基类的成员。

一个类作用域嵌套的例子:

#include
using namespace std;
class A{
public:
void func();
public:
int n;
};
void A::func(){ cout<<“c.biancheng.net”<<endl; }
class B: public A{
public:
int n;
int m;
};
class C: public B{
public:
int n;
int x;
};
int main(){
C obj;
obj.n;
obj.func();
cout<<sizeof©<<endl;
return 0;
}

运行结果:
c.biancheng.net
20

本例中,B 继承自 A,C继承自 B,它们作用域的嵌套关系如下图所示:

img

obj 是 C 类的对象,通过 obj 访问成员变量 n 时,在 C 类的作用域中就能够找到了 n 这个名字。虽然 A 类和 B 类都有名字 n,但编译器不会到它们的作用域中查找,所以是不可见的,也即派生类中的 n 遮蔽了基类中的 n。

通过 obj 访问成员函数 func() 时,在 C 类的作用域中没有找到 func 这个名字,编译器继续到 B 类的作用域(外层作用域)中查找,仍然没有找到,再继续到 A 类的作用域中查找,结果就发现了 func 这个名字,于是查找结束,编译器决定调用 A 类作用域中的 func() 函数。

这个过程叫做名字查找(name lookup),也就是在作用域链中寻找与所用名字最匹配的声明(或定义)的过程。

对于成员变量这个过程很好理解,对于成员函数要引起注意,编译器仅仅是根据函数的名字来查找的,不会理会函数的参数。换句话说,一旦内层作用域有同名的函数,不管有几个,编译器都不会再到外层作用域中查找,编译器仅把内层作用域中的这些同名函数作为一组候选函数;这组候选函数就是一组重载函数。

说白了,只有一个作用域内的同名函数才具有重载关系,不同作用域内的同名函数是会造成遮蔽,使得外层函数无效。派生类和基类拥有不同的作用域,所以它们的同名函数不具有重载关系。

我们不妨再来回顾一下上节的例子:

#include
using namespace std;
//基类Base
class Base{
public:
void func();
void func(int);
};
void Base::func(){ cout<<“Base::func()”<<endl; }
void Base::func(int a){ cout<<“Base::func(int)”<<endl; }
//派生类Derived
class Derived: public Base{
public:
void func(char *);
void func(bool);
};
void Derived::func(char *str){ cout<<“Derived::func(char *)”<<endl; }
void Derived::func(bool is){ cout<<“Derived::func(bool)”<<endl; }
int main(){
Derived d;
d.func(“c.biancheng.net”);
d.func(true);
d.func(); //compile error
d.func(10); //compile error
d.Base::func();
d.Base::func(100);
return 0;
}

虽然 Derived 类和 Base 类都有同名的 func 函数,但它们位于不同的作用域,Derived 类的 func 会遮蔽 Base 类的 func。d 是 Derived 类的对象,调用 func 函数时,编译器会先在 Derived 类中查找“func”这个名字,发现有两个,也即void func(char*)和void func(bool),这就是一组候选函数。

执行到第 26、27 行代码时,在候选函数中没有找到匹配的函数,所以调用失败,这时编译器会抛出错误信息,而不是再到 Base 类中查找同名函数。

C++继承时的对象内存模型

已剪辑自: http://c.biancheng.net/view/vip_2273.html

在《C++对象的内存模型》一节中我们讲解了没有继承时对象内存的分布情况。这时的内存模型很简单,成员变量和成员函数会分开存储:

  • 对象的内存中只包含成员变量,存储在栈区或堆区(使用 new 创建对象);
  • 成员函数与对象内存分离,存储在代码区。

当存在继承关系时,内存模型会稍微复杂一些。

继承时的内存模型

有继承关系时,派生类的内存模型可以看成是基类成员变量和新增成员变量的总和,而所有成员函数仍然存储在另外一个区域——代码区,由所有对象共享。请看下面的代码:

#include
using namespace std;
//基类A
class A{
public:
A(int a, int b);
public:
void display();
protected:
int m_a;
int m_b;
};
A::A(int a, int b): m_a(a), m_b(b){}
void A::display(){
printf(“m_a=%d, m_b=%d\n”, m_a, m_b);
}
//派生类B
class B: public A{
public:
B(int a, int b, int c);
void display();
protected:
int m_c;
};
B::B(int a, int b, int c): A(a, b), m_c©{ }
void B::display(){
printf(“m_a=%d, m_b=%d, m_c=%d\n”, m_a, m_b, m_c);
}
int main(){
A obj_a(99, 10);
B obj_b(84, 23, 95);
obj_a.display();
obj_b.display();
return 0;
}

obj_a 是基类对象,obj_b 是派生类对象。假设 obj_a 的起始地址为 0X1000,那么它的内存分布如下图所示:

img

假设 obj_b 的起始地址为 0X1100,那么它的内存分布如下图所示:

img

可以发现,基类的成员变量排在前面,派生类的排在后面。

为了让大家理解更加透彻,我们不妨再由 B 类派生出一个 C 类:

//声明并定义派生类C
class C: public B{
public:
C(char a, int b, int c, int d);
public:
void display();
private:
int m_d;
};
C::C(char a, int b, int c, int d): B(a, b, c), m_d(d){ }
void C::display(){
printf(“m_a=%d, m_b=%d, m_c=%d, m_d=%d\n”, m_a, m_b, m_c, m_d);
}
//创建C类对象obj_c
C obj_c(84, 23, 95, 60);
obj_c.display();

假设 obj_c 的起始地址为 0X1200,那么它的内存分布如下图所示:

img

成员变量按照派生的层级依次排列,新增成员变量始终在最后。

有成员变量遮蔽时的内存分布

更改上面的 C 类,让它的成员变量遮蔽 A 类和 B 类的成员变量:

//声明并定义派生类C
class C: public B{
public:
C(char a, int b, int c, int d);
public:
void display();
private:
int m_b; //遮蔽A类的成员变量
int m_c; //遮蔽B类的成员变量
int m_d; //新增成员变量
};
C::C(char a, int b, int c, int d): B(a, b, c), m_b(b), m_c©, m_d(d){ }
void C::display(){
printf(“A::m_a=%d, A::m_b=%d, B::m_c=%d\n”, m_a, A::m_b, B::m_c);
printf(“C::m_b=%d, C::m_c=%d, C::m_d=%d\n”, m_b, m_c, m_d);
}
//创建C类对象obj_c
C obj_c(84, 23, 95, 60);
obj_c.display();

假设 obj_c 的起始地址为 0X1300,那么它的内存分布如下图所示:

img

当基类 A、B 的成员变量被遮蔽时,仍然会留在派生类对象 obj_c 的内存中,C 类新增的成员变量始终排在基类 A、B 的后面。

总结:在派生类的对象模型中,会包含所有基类的成员变量。这种设计方案的优点是访问效率高,能够在派生类对象中直接访问基类变量,无需经过好几层间接计算。

借助指针突破访问权限的限制,访问private、protected属性的成员变量

已剪辑自: http://c.biancheng.net/view/vip_2279.html

我们都知道,C++ 不允许通过对象来访问 private、protected 属性的成员变量,例如:

#include
using namespace std;
class A{
public:
A(int a, int b, int c);
private:
int m_a;
int m_b;
int m_c;
};
A::A(int a, int b, int c): m_a(a), m_b(b), m_c©{ }
int main(){
A obj(10, 20, 30);
int a = obj.m_a; //Compile Error
A *p = new A(40, 50, 60);
int b = p->m_b; //Compile Error
return 0;
}

这段代码说明了,无论通过对象变量还是对象指针,都不能访问 private 属性的成员变量。

不过 C++ 的这种限制仅仅是语法层面的,通过某种“蹩脚”的方法,我们能够突破访问权限的限制,访问到 private、protected 属性的成员变量,赋予我们这种“特异功能”的,正是强大而又灵活的指针(Pointer)。

使用偏移

在对象的内存模型中,成员变量和对象的开头位置会有一定的距离。以上面的 obj 为例,它的内存模型为:

img

图中假设 obj 对象的起始地址为 0X1000,m_a、m_b、m_c 与对象开头分别相距 0、4、8 个字节,我们将这段距离称为偏移(Offset)。一旦知道了对象的起始地址,再加上偏移就能够求得成员变量的地址,知道了成员变量的地址和类型,也就能够轻而易举地知道它的值。

当通过对象指针访问成员变量时,编译器实际上也是使用这种方式来取得它的值。为了说明问题,我们不妨将上例中成员变量的访问权限改为 public,再来执行第 18 行的语句:

int b = p->m_b;

此时编译器内部会发生类似下面的转换:

int b = (int)( (int)p + sizeof(int) );

p 是对象 obj 的指针,(int)p将指针转换为一个整数,这样才能进行加法运算;sizeof(int)用来计算 m_b 的偏移;(int)p + sizeof(int)得到的就是 m_b 的地址,不过因为此时是int类型,所以还需要强制转换为int *类型;开头的*用来获取地址上的数据。

img

如果通过 p 指针访问 m_a:

int a = p -> m_a;

那么将被转换为下面的形式:

int a = * (int*) ( (int)p + 0 );

经过简化以后为:

int a = (int)p;

突破访问权限的限制

上述的转换过程是编译器自动完成的,当成员变量的访问权限为 private 时,我们也可以手动转换,只要能正确计算偏移即可,这样就突破了访问权限的限制。

修改上例中的代码,借助偏移来访问 private 属性的成员变量:

#include
using namespace std;
class A{
public:
A(int a, int b, int c);
private:
int m_a;
int m_b;
int m_c;
};
A::A(int a, int b, int c): m_a(a), m_b(b), m_c©{ }
int main(){
A obj(10, 20, 30);
int a1 = (int)&obj;
int b = (int)( (int)&obj + sizeof(int) );
A *p = new A(40, 50, 60);
int a2 = (int)p;
int c = (int)( (int)p + sizeof(int)*2 );

cout<<“a1=”<<a1<<“, a2=”<<a2<<“, b=”<<b<<“, c=”<<c<<endl;
return 0;
}

运行结果:
a1=10, a2=40, b=20, c=60

前面我们说 C++ 的成员访问权限仅仅是语法层面上的,是指访问权限仅对取成员运算符.->起作用,而无法防止直接通过指针来访问。你可以认为这是指针的强大,也可以认为是 C++ 语言设计的瑕疵。

本节的目的不是为了访问到 private、protected 属性的成员变量,这种“花拳绣腿”没有什么现实的意义,本节主要是让大家明白编译器内部的工作原理,以及指针的灵活运用。

C++将派生类赋值给基类(向上转型)

已剪辑自: http://c.biancheng.net/view/2284.html

在 C/C++ 中经常会发生数据类型的转换,例如将 int 类型的数据赋值给 float 类型的变量时,编译器会先把 int 类型的数据转换为 float 类型再赋值;反过来,float 类型的数据在经过类型转换后也可以赋值给 int 类型的变量。

数据类型转换的前提是,编译器知道如何对数据进行取舍。例如:

int a = 10.9;
printf(“%d\n”, a);

输出结果为 10,编译器会将小数部分直接丢掉(不是四舍五入)。再如:

float b = 10;
printf(“%f\n”, b);

输出结果为 10.000000,编译器会自动添加小数部分。

类其实也是一种数据类型,也可以发生数据类型转换,不过这种转换只有在基类和派生类之间才有意义,并且只能将派生类赋值给基类,包括将派生类对象赋值给基类对象、将派生类指针赋值给基类指针、将派生类引用赋值给基类引用,这在 C++ 中称为向上转型(Upcasting)。相应地,将基类赋值给派生类称为向下转型(Downcasting)。

向上转型非常安全,可以由编译器自动完成;向下转型有风险,需要程序员手动干预。本节只介绍向上转型,向下转型将在后续章节介绍。 向上转型和向下转型是面向对象编程的一种通用概念,它们也存在于 JavaC# 等编程语言中。

将派生类对象赋值给基类对象

下面的例子演示了如何将派生类对象赋值给基类对象:

#include
using namespace std;
//基类
class A{
public:
A(int a);
public:
void display();
public:
int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
cout<<“Class A: m_a=”<<m_a<<endl;
}
//派生类
class B: public A{
public:
B(int a, int b);
public:
void display();
public:
int m_b;
};
B::B(int a, int b): A(a), m_b(b){ }
void B::display(){
cout<<“Class B: m_a=”<<m_a<<“, m_b=”<<m_b<<endl;
}
int main(){
A a(10);
B b(66, 99);
//赋值前
a.display();
b.display();
cout<<“--------------”<<endl;
//赋值后
a = b;
a.display();
b.display();
return 0;
}

运行结果:
Class A: m_a=10
Class B: m_a=66, m_b=99

Class A: m_a=66
Class B: m_a=66, m_b=99

本例中 A 是基类, B 是派生类,a、b 分别是它们的对象,由于派生类 B 包含了从基类 A 继承来的成员,因此可以将派生类对象 b 赋值给基类对象 a。通过运行结果也可以发现,赋值后 a 所包含的成员变量的值已经发生了变化。

赋值的本质是将现有的数据写入已分配好的内存中,对象的内存只包含了成员变量,所以对象之间的赋值是成员变量的赋值,成员函数不存在赋值问题。运行结果也有力地证明了这一点,虽然有a=b;这样的赋值过程,但是 a.display() 始终调用的都是 A 类的 display() 函数。换句话说,对象之间的赋值不会影响成员函数,也不会影响 this 指针。

将派生类对象赋值给基类对象时,会舍弃派生类新增的成员,也就是“大材小用”,如下图所示:

img

可以发现,即使将派生类对象赋值给基类对象,基类对象也不会包含派生类的成员,所以依然不同通过基类对象来访问派生类的成员。对于上面的例子,a.m_a 是正确的,但 a.m_b 就是错误的,因为 a 不包含成员 m_b。

这种转换关系是不可逆的,只能用派生类对象给基类对象赋值,而不能用基类对象给派生类对象赋值。理由很简单,基类不包含派生类的成员变量,无法对派生类的成员变量赋值。同理,同一基类的不同派生类对象之间也不能赋值。

要理解这个问题,还得从赋值的本质入手。赋值实际上是向内存填充数据,当数据较多时很好处理,舍弃即可;本例中将 b 赋值给 a 时(执行a=b;语句),成员 m_b 是多余的,会被直接丢掉,所以不会发生赋值错误。但当数据较少时,问题就很棘手,编译器不知道如何填充剩下的内存;如果本例中有b= a;这样的语句,编译器就不知道该如何给变量 m_b 赋值,所以会发生错误。

将派生类指针赋值给基类指针

除了可以将派生类对象赋值给基类对象(对象变量之间的赋值),还可以将派生类指针赋值给基类指针(对象指针之间的赋值)。我们先来看一个多继承的例子,继承关系为:

img

下面的代码实现了这种继承关系:

#include
using namespace std;
//基类A
class A{
public:
A(int a);
public:
void display();
protected:
int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
cout<<“Class A: m_a=”<<m_a<<endl;
}
//中间派生类B
class B: 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<<“Class B: m_a=”<<m_a<<“, m_b=”<<m_b<<endl;
}
//基类C
class C{
public:
C(int c);
public:
void display();
protected:
int m_c;
};
C::C(int c): m_c©{ }
void C::display(){

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
生类指针赋值给基类指针(对象指针之间的赋值)。我们先来看一个多继承的例子,继承关系为:

img

下面的代码实现了这种继承关系:

#include
using namespace std;
//基类A
class A{
public:
A(int a);
public:
void display();
protected:
int m_a;
};
A::A(int a): m_a(a){ }
void A::display(){
cout<<“Class A: m_a=”<<m_a<<endl;
}
//中间派生类B
class B: 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<<“Class B: m_a=”<<m_a<<“, m_b=”<<m_b<<endl;
}
//基类C
class C{
public:
C(int c);
public:
void display();
protected:
int m_c;
};
C::C(int c): m_c©{ }
void C::display(){

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-VqcHvEdv-1713178696238)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 17
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值