目录
前言
这是一篇关于C++面向对象的小总结以及实验过程记录,可能会有些遗漏,甚至是错误。如果你发现了本文的错误欢迎您指出,博主一定会仔细研究订正!
单类
构造析构及初始化列表
先上个例子:
class Point {
private:
int x_;
int y_;
public:
Point(int x, int y):y_(y),x_(y_+1) { } //显然x会先初始化 就与想要的答案有出入
void print() {
cout << x_ << " " << y_ << endl;
}
};
int main() {
Point p(2, 3);
p.print();
return 0;
}
- 首先构造函数的含义就不再赘述,要明白的点是:一个类,默认有无参构造函数、析构函数、浅拷贝构造函数、赋值构造函数。
- 一个类的方法定义可以类内定义也可以类外定义,具体怎么写这里不在细讲。
- 初始化列表会按照成员函数定义的顺序去调用,比如上面的例子,造成错误的原因就是忽略了初始化的顺序
- 析构函数是在一个对象即将释放内存的时候调用,这里可以和linux下多进程的处理机制一样,其实就是在“交代后事”的一个遗言时间(
突然想到狼人杀惹) - 参数列表初始化的写法是优先于构造函数调用的!比如你有一些常量类型的成员变量只能用参数列表初始化
- 类中static成员变量,必须在类外初始化,也是最早的,但是是所有对象共享的
浅拷贝深拷贝
先上个拷贝构造的样子:
Point(const Point &x) {
...
}
注:加不加const无所谓具体区别就是在于能不能接收一个常量的对象,具体去理解const的用法;至于引用加不加的问题后面会讲,这关乎会不会产生一个临时对象
- 浅拷贝就是每个对象成员变量按值赋值,这很好理解,但一旦成员变量有指针、字符串、对象指针就会有问题了,就会导致两个对象的成员变量指向同一个内存,当释放这块内存时必然会有一个对象出错(这块地址已经释放了)。
- 深拷贝就是在构造的时候再动态的开辟一块内存,然后内存的存的值相同,然后释放的时候都是释放自己的
赋值运算符重载
先上个例子:
int operator = (const A& obj) const{
return ...;
}
这个语法提供了C++一种静态多态的东西,中间是运算符,注意sizeof、. 、.*、::、?:等这几个运算符不能重载,理解的时候可以当成前面有个当前类的对象于后面的一个"狗"进行运算。而当你想前面是一个“狗”,而后面是一个当前对象的时候就要用到友元了,后面讲。补充一点,这里的写法也要注意,不然容易产生临时对象。
几个重要的临时对象例子解析
例1:默认拷贝构造产生临时对象
class A{
public:
A(double a = 0) {
c = a;
cout << "构造" << a << endl;
}
operator double() {
cout << "强转" << endl;
return c;
}
~A() {
cout << "析构" << c << endl;
}
double c;
};
int main() {
A a; //构造
a = 1.1; //构造 赋值 析构
double s = a; //强转 了解一下写法就行operator Type() {return ...;}
cout << s << endl;
return 0;
}
注意1.1由于是赋值给一个对象,会先查询这个类有没有相应的构造函数,有则去生成一个临时对象(内存属于栈区,域属于本行),相当于变成这句:a = A(1.1);所有赋值完马上析构
例2:赋值运算符重载、拷贝构造设置不当产生临时对象、类内对象初始化列表
class Point {
private:
int x, y;
public:
Point() {
cout << "Point 默认构造函数" << " x = " << x << " y = " << y << " memory = " << this << endl;
}
Point(int x, int y):x(x), y(y){
cout << "Point 构造函数 ";
cout << " x = " << x << " y = " << y << " memory = " << this <<endl;
}
Point(const Point &x);
int getX() const {
return this->x;
}
int getY() const {
return this->y;
}
Point operator = (const Point& A) { //这里也产生了一个临时对象
cout << "赋值函数 " << A.getX() << " " << A. getY() << " memory = " << this << endl;
this->x = A.getX();
this->y = A.getY();
return *this; //又产生了一个临时对象
}
~Point() {
cout << "Point 析构函数" << " x = " << x << " y = " << y << " memory = " << this << endl;
}
};
Point::Point(const Point &p) {
x = p.x;
y = p.y;
cout << "point 拷贝构造函数" << " x = " << x << " y = " << y << " memory = " << this <<endl;
}
Point id(100, 100); //全局对象(堆空间)
#define TAG 3
class Distance {
private:
Point p1, p2; //这两个也会初始化
double dis;
public:
#if TAG == 1 || TAG == 2
Distance(Point p1, Point p2);
#else
Distance(Point &p1, Point &p2);
#endif
double getDis() {return dis;};
};
#if TAG==0
//引用传参 构造函数的形参列表不会产生临时对象
Distance::Distance(Point &p1, Point &p2):p1(p1),p2(p2) {
cout << "Distance 构造函数 " << " memory = " << this << endl;
dis = 100;
}
#elif TAG==1
//拷贝传参 构造函数的形参列表中产生额外的临时对象
Distance::Distance(Point p1, Point p2):p1(p1),p2(p2) {
cout << "Distance 构造函数 " << " memory = " << this << endl;
dis = 100;
}
#elif TAG==2
Distance::Distance(Point p1, Point p2) {
cout << endl;
this->p1 = p1;
cout << endl;
this->p2 = p2;
cout << endl;
cout << "Distance 构造函数 " << " memory = " << this << endl;
dis = 100;
}
#else
Distance::Distance(Point &p1=id, Point &p2=id) {
cout << endl;
this->p1 = p1;
cout << endl;
this->p2 = p2;
cout << endl;
cout << "Distance 构造函数 " << " memory = " << this << endl;
dis = 100;
p1 = p2;
}
#endif
int main() {
Point x(1, 2), y(3, 4);
Point c = x;
Point &e = y;
cout << endl;
Distance d(x, y);
cout << x.getX() << " " << x.getY() << endl;
cout << y.getX() << " " << y.getY() << endl;
return 0;
}
注意这Point类如果改成以下就会在对应位置产生临时对象:
Point operator = (const Point A) { //这里也产生了一个临时对象
cout << "赋值函数 " << A.getX() << " " << A. getY() << " memory = " << this << endl;
this->x = A.getX();
this->y = A.getY();
return *this; //又产生了一个临时对象
}
抓住的关键点是看形参接收的是一个引用还是对象
例3:const修饰的引用传递产生临时对象
void swap6(const double &a, const double &b) {
double tmp = a;
// a = b;
// b = tmp;
cout << (&a) << endl;
}
int main() {
int a1 = 1, b1 = 2;
cout << &a1 << endl;
swap6(a1, b1); //利用const修饰的变量自动转型后会产生临时对象 故地址不一致
cout << a1 << " " << b1 << endl;
return 0;
}
- 如果被const修饰的类型使用起来要注意,因为在自动转型时引用就会失效了,产生临时对象。
- 接收是const对象引用的情况下临时对象产生:类型不匹配(自动转型)
例4:函数传递对象时的临时对象产生及不同构造写法区别
class node {
public:
node() {
cout << "constructor : " << "mem=" << static_cast<void*>(this) << endl;
}
node(const node& T) {
cout << "copy constructor : " << "me mem=" << static_cast<void*>(this) << " ohter side : " << const_cast<node*>(&T) << endl;
}
node& operator = (const node& T) { //注意这里引用传回和拷贝传回对象的区别
cout << "assign function : " << "mem=" << static_cast<void*>(this) << endl;
return *this;
}
~node() {
cout << "destructor : " << "mem=" << static_cast<void*>(this) << endl;
}
};
node f1() {
node *c = new node(); //相当于node c() 或 node c 只不过开辟了堆空间的地址
cout << "f() : " << c <<endl;
return *c;
}
node f() {
return node();
}
int main() {
node obj = f(); //为什么这里没调用拷贝构造(因为是栈区的变量,所以直接赋值,如果是new出来的就会传递的时候多一次临时变量)
cout << endl;
node obj1 = f1(); //这样就会调用拷贝构造
//上面两个区别就相当于 一个是node() 一个是*(new node)
cout << endl;
obj = f(); //调赋值 本身地址不变
cout << endl;
node c;
node s(c); //拷贝
cout << endl;
node d = *new node(); //同第2个一个道理
cout << endl;
return 0;
}
输出:
constructor : mem=0x6cfede
constructor : mem=0xc015c0
f() : 0xc015c0
copy constructor : me mem=0x6cfedd ohter side : 0xc015c0
constructor : mem=0x6cfedf
assign function : mem=0x6cfede
destructor : mem=0x6cfedf
constructor : mem=0x6cfedc
copy constructor : me mem=0x6cfedb ohter side : 0x6cfedc
constructor : mem=0xc015e0
copy constructor : me mem=0x6cfeda ohter side : 0xc015e0
destructor : mem=0x6cfeda
destructor : mem=0x6cfedb
destructor : mem=0x6cfedc
destructor : mem=0x6cfedd
destructor : mem=0x6cfede
总结
1,一个函数new出来的对象在通过值传递回主调函数的时候,会形成一个临时对象,相比于用node c或node c = new node()开辟的是栈空间的内存,所以
2,不会产生临时对象,相应的如果是传回栈内存的引用属于右值引用的范畴,要用
一个常量对象接收或者用c++11的&&语法接收右值
3,const的修饰的类型能够接收常量对象以及非常量对象 否则只能接收常量对象
const具体用法
const修饰符:
放在左边:指针常量(指针可改,内容不可改)
放在右边:常量指针(指针不可改,内容可改)
两边都要:指向常量的常量指针(都不可改)
看个语法浅析的例子:
#include <bits/stdc++.h>
using namespace std;
#include <cxxabi.h>
const string GetClearName(const char* name)
{
int status = -1;
char* clearName = abi::__cxa_demangle(name, NULL, NULL, &status);
const char* const demangledName = (status==0) ? clearName : name;
string ret_val(demangledName);
free(clearName);
return ret_val;
}
/**
* const修饰符:
* 放在*左边:指针常量(指针可改,内容不可改)
* 放在*右边:常量指针(指针不可改,内容可改)
* *两边都要:指向常量的常量指针(都不可改)
*/
int main() {
const int a1 = 10; //常量 [1]
// a1 = 11; 编译错误 不能修改常量 [2]
int a2 = 10;
int a3 = 11;
const int* p1 = &a2; //指针常量 顾名思义:指针指向常量,即指针内容不能变,而指针能变 [3]
cout << "p1.val : " << *p1 << endl;
// *p1 = 12; 编译错误 不能修改指针常量的内容 [4]
p1 = &a3; //p1是指针常量所以指针本身可以改指向,但是它指向的内容一直不能动 [5]
cout << "p1.val : " << *p1 << endl;
// *p1 = 13; 编译错误 原因和[4]的原因一致[6]
int const *p2 = &a2; //指针常量的另一种修饰法 [7]
// *p2 = 12; 编译错误
p2 = &a3; //正确
cout << endl;
int a4 = 21;
int a5 = 22;
int* const q1 = &a4; //常量指针 指针的指向第一次就固定,不能修改,但能够修改内部内容 [8]
// q1 = &a5; 编译错误 类似于数组首地址也是一个常量指针 不能改指向 [9]
cout << "q1.val : " << *q1 << endl;
*q1 = 23; //正确 常量指针的值能够修改
cout << "q1.val : " << *q1 << endl;
cout << endl;
int a6 = 31;
int a7 = 32;
int const * const v = &a6; //常量指针内容也为常量 [10]
// *v = 33; 编译错误
// v = &a7; 编译错误
cout << endl;
char s1[] = "hello";
char s2[] = "word!";
char *ptr1 = s1; //普通变量指针
const char *ptr2 = s1; //指针常量
// ptr2[0] = 'w'; 内容不能改
cout << "ptr2.val : " << ptr2 << endl;
ptr2 = s2; //正确
cout << "ptr2.val : " << ptr2 << endl;
char const *ptr3 = s1; //指针常量的另一种写法
cout << "ptr3.val : " << ptr3 << endl;
ptr3 = ptr2; //正确
cout << "ptr3.val : " << ptr3 << endl;
cout << endl;
char* const qtr1 = s1;
// qtr1 = s2; 编译错误
cout << "qtr.val : " << qtr1 << endl;;
qtr1[0] = 's';
cout << "qtr.val : " << qtr1 << endl;
cout << endl;
cout << "s1 : " << s1 << endl;
cout << "s2 : " << s2 << endl;
cout << endl;
cout << "利用typeid(a).name()查看类型:" << endl; //由于g++的缘故要调用cxxabi库中的函数自己然后利用源码转型
cout << "p1 typename : " << GetClearName(typeid(p1).name()) << endl;
cout << "q1 typename : " << GetClearName(typeid(q1).name()) << endl;
cout << "v typename : " << GetClearName(typeid(v).name()) << endl;
cout << "s1 typename : " << GetClearName(typeid(s1).name()) << endl;
cout << "ptr1 typename : " << GetClearName(typeid(ptr1).name()) << endl;
cout << "ptr2 typename : " << GetClearName(typeid(ptr2).name()) << endl;
cout << "ptr3 typename : " << GetClearName(typeid(ptr3).name()) << endl;
cout << "qtr1 typename : " << GetClearName(typeid(qtr1).name()) << endl;
cout << endl;
return 0;
}
输出:
q1.val : 21
q1.val : 23
ptr2.val : hello
ptr2.val : word!
ptr3.val : hello
ptr3.val : word!
qtr.val : hello
qtr.val : sello
s1 : sello
s2 : word!
利用typeid(a).name()查看类型:
p1 typename : int const*
q1 typename : int*
v typename : int const*
s1 typename : char [6]
ptr1 typename : char*
ptr2 typename : char const*
ptr3 typename : char const*
qtr1 typename : char*
const修饰的方法及常对象
先上例子:
class A {
public:
A() {}
int a;
static int b;
void f() const {
cout << "b" << endl;
}
void f() {
cout << "a" << endl;
}
};
int A::b = 2;
int main() {
const A c;
// c.a = 1; //c为常对象 不能修改内存的东西
c.b = 1;
c.f(); //常对象只能调用常方法 因为常方法不会修改内存的东西 常对象优先调用常方法
A s;
s.f(); //优先调用const方法
return 0;
}
常方法不能够修改该对象内存的东西,以及一个常对象的内存上的东西也不应该被修改。个人理解:一个被const修饰的方法相当于内容都是只读的,不能去修改。
注:这里的const和java的final不太一样,之后讲到继承时const方法若是虚函数是会被覆盖的只不过一旦某个位置不一样就不是覆盖了而是隐藏。
常对象优先调用常方法,普通对象优先调用普通方法
初始化顺序
class Point {
private:
int x_;
int y_;
public:
Point(int x, int y):y_(y),x_(y_+1) { } //显然x会先初始化 就与想要的答案有出入
void print() {
cout << x_ << " " << y_ << endl;
}
};
int main() {
Point p(2, 3);
p.print();
return 0;
}
- 如果数据成员是对象,则初始化和定义顺序有关,和初始化列表无关
- 初始化列表在初始化时和按照定义的顺序进行初始化
- 初始化的顺序:
- 定义初始化
- 列表初始化
- 构造函数
多类嵌套
给个例子:
class A {
public:
A() {
}
};
class B {
public :
B(int a) {
}
};
class C{
A a;
// 不能是B b(1); 只能是B* b = new B(1); 或B b = *(new B(1)); 因为这样内存才会在堆中
B b;
public :
C(int x) : b(x) { //没默认构造 必须这样初始化 不能放到定义成员变量中!!!
}
};
int main() {
C c(1);
return 0;
}
注意一下嵌套时候的初始化问题就好了
多类继承
权限及继承方式的权限问题
- class定义的类默认所有方法都是私有的!包括默认的!!!
- struct默认是公有的!
- class继承方式:
class Parent {
//访问权限(不一定要写 struct默认public class默认private)
public:
int pub; //内 外 子 访问
protected:
int pro; //内 子 访问
private:
int pri; //内 访问
};
/**继承方式(必须写)**/
/**
* 公有继承:
* public成员内部、外部、子类能访问;(在子类也是public)
* protected成员内部、子类能访问;(在子类也是protected)
* private成员都不能访问;(在子类不可见)
* 总结:公有继承除了父类的私有成员不可见,其余父类是啥权限子类也是啥权限。
*
* 保护继承:
* public成员内部、子类能访问;(在子类为protected)
* protected成员内部、子类能访问;(在子类为protected)
* private成员都不能访问;(在子类不可见)
* 总结:保护继承除了私有成员不可见,其余父类是啥权限子类最多只会以保护权限接收
*
* 私有继承:
* public成员内部能访问;(在子类为private)
* protected成员内部能访问;(在子类为private)
* private成员都不能访问;(在子类不可见)
* 总结:私有继承除了父类的私有成员不可见,其余统统转为private权限
*/
//公有继承
class Child1:public Parent {
};
//保护继承
class Child2:protected Parent {
};
//私有继承
class Child3:private Parent {
};
- 如果是struct默认的继承方式是公有的可以省略
构造析构继承的调用顺序
//补充:
//string内部默认深拷贝
//::域访问符前面没任何命名空间或者某个标记表示访问全局的
//内存分配采用向下增长的模式 即高地址到低地址
class Person {
public:
Person() {
cout << "Person Default Constructor : memory=" << static_cast<void*>(this) <<endl;
}
Person(const string& name, int age, const string& sex): name(name), age(age), sex(sex){
cout << "Person Constructor : memory=" << static_cast<void*>(this) <<endl;
}
void getInfo() {
cout << endl;
cout << "Person getInfo : memory=" << static_cast<void*>(this) <<endl;
cout << "name: " << name << endl;
cout << "sex: " << sex << endl;
cout << "age: " << age << endl;
cout << endl;
}
~Person() {
cout << "Person Destructor : memory=" << static_cast<void*>(this) <<endl;
}
private:
string name;
string sex;
int age;
};
class Student : public Person {
public:
//利用初始化列表先去把父类的参数初始化后回来调用自己的构造
Student(const string& name, int age, const string& sex, int num): num(num), Person(name, age, sex) {
cout << "Student Constructor : memory=" << static_cast<void*>(this) <<endl;
}
//如果构造函数的初始化列表没有调父类的构造函数会默认调无参的父类构造函数,没有则会报错
Student(int num): num(num) {
cout << "Student Constructor : memory=" << static_cast<void*>(this) <<endl;
}
~Student() {
cout << "Student Destructor : memory=" << static_cast<void*>(this) <<endl;
}
private:
int num;
};
int main() {
Person p1("Lili", 11, "famale");
Student s1("Lilei", 17, "male", 211);
p1.getInfo();
s1.getInfo();
{
Student s2(211);
Person *s1 = &s2;
}
cout << endl;
{
Student *s2 = new Student(212);
Person *s1 = s2;
delete s1; //只调用了基类方法 若要发生多态的析构 则基类的析构要加上virtual
}
cout << endl;
return 0;
}
输出:
Person Constructor : memory=0x73fcf0
Person Constructor : memory=0x73fca0
Student Constructor : memory=0x73fca0
Person getInfo : memory=0x73fcf0
name: Lili
sex: famale
age: 11
Person getInfo : memory=0x73fca0
name: Lilei
sex: male
age: 17
Person Default Constructor : memory=0x73fc50
Student Constructor : memory=0x73fc50
Student Destructor : memory=0x73fc50
Person Destructor : memory=0x73fc50
Person Default Constructor : memory=0xc42560
Student Constructor : memory=0xc42560
Person Destructor : memory=0xc42560
Student Destructor : memory=0x73fca0
Person Destructor : memory=0x73fca0
Person Destructor : memory=0x73fcf0
- 父子类构造的顺序:
先父类(利用初始化列表)再子类 - 父子类析构的顺序:
先子类再父类(类似于linux线程处理模型,把自己kill然后再回到上级kill,因为要留个父类收拾残局) - 一个域中的静态构造对象(栈区内存的)往往是先析构后面创建的再析构前面创建的,因为栈区的空间后进先出
- 注意如果发生多态,基类中的析构却没有加入虚函数表即加上virtual,则这个上转型的子类对象只会调一次基类的析构。注意别搞混淆了!是发生多态的对象!普通继承父类的对象并且没发生上转型就仍可以调用自己的析构以及父类的析构。
正常顺序应是这样(包括正确使用多态即在基类析构加virtual后):
派生类构造的顺序:
- 1,基类构造函数
- 2,派生类包含对象构造函数
- 3,派生类构造函数
派生类析构顺序:(因为派生类调用析构函数后才会释放内存,从而导致其包含的对象析构)
- 1,派生类析构函数
- 2,派生类包含对象析构函数
- 3,基类析构函数
父子类间的静态成员
class A {
public:
A(int x):x(x) {
}
// private:
int x;
static int y;
};
class B : public A {
public:
B(int x):A(x) {}
static int y;
};
int A::y = 1;
int B::y = 100;
int main() {
B::A::y = 10; //通过B类修改A类中的静态成员1
A s1(1);
A s2(2);
cout << s1.y << endl;
cout << s2.y << endl;
B t1(10);
B t2(11);
t1.A::y = 1; //通过B类修改A类中的静态成员2
cout << t1.y << endl; //两个y不是同一个y
cout << t2.y << endl;
return 0;
}
根据上面的实验,我们可以得出类静态成员的一些特性:
- 静态成员会存在覆盖的情况,一般是整个家族共享的一个名称的,除非某个子孙发生覆盖
- 静态的成员不占用类对象的内存,其实相当于这个类可以访问到它但是却不属于它,属于一片静态区
多继承
形式如下:
class A : public B, public C {
...
}
Ps:传说中的一个人有多个爸爸,感觉没java中的合理,只不过java用接口来达到这个多继承的特性
虚函数
先看个例子:
class C {
virtual void fun() {}
};
class B {
virtual void fun(){}
};
class A : public B, public C {
void fun() {}
};
有定义虚函数即类中有virtual的方法,在内存的首部都有个虚函数表,里面存的是函数指针
重载、覆写、重定义(隐藏)
许多编程语言都有多态,以上几个就是类中多态发生的一些充分不必要条件,当然,
多态发生的条件可能还要有上转型等条件。
重载:是发生在同一个域内的,函数名相同,参数列表不同,当然这个重载由于有自动转型的原因你不能写一些有二义性的代码,比如:
void f(double) {
cout << "1" << endl;
}
void f(char) {
cout << "2" << endl;
}
int main()
{
f(1); //编译错误
f(1.1);
f('c');
return 0;
}
覆盖:父类有虚函数修饰,子类写一个和它一模一样的(有没有virtual修饰无所谓,编译器默认加上virtual),注意是一模一样的!const修饰过的和没修饰的方法是两个方法,也就是重载而不是覆盖!看个例子:
class Base
{
public:
virtual void print() const = 0;
};
class Test : public Base
{
public:
//注意这两个是重载
void print();
void print() const; //只有这句是对抽象类的实现,也就是覆盖了纯虚函数
};
void Test::print()
{
cout << "Test::print()" << endl;
}
void Test::print() const
{
cout << "Test::print() const" << endl;
}
void main()
{
Base* pChild = new Test();
pChild->print(); //调它实现的那个方法 注意这里要区别于多态
const Test obj;
obj.print(); //常量类优先调常量方法
Test obj1;
obj1.print(); //普通类优先调普通方法
Test* pOwn = new Test();
pOwn->print();
}
输出:
Test::print() const
Test::print() const
Test::print()
Test::print()
重定义(隐藏):只有父类中不是用virtual修饰的,子类只要函数名一样,参数列表和返回值可以不一样,就是发生了重定义。比如:
class C {
public:
void fun(int a) {
cout << "1" << endl;
}
};
class B : public C{
public:
int fun(){
cout << "2" << endl;
return 1;
}
};
int main() {
C c;
B b;
c.fun(1); //C的
b.fun(); //B的
// b.fun(1); //出错,因为父类的方法被隐藏了
b.C::fun(1);
return 0;
}
虚继承
虚继承的对象在内存空间上会开一个虚继承表,这个表会类似于指针偏移一样,当使用到父类的东西直接去查指针里面的东西。例如:
/**
* 虚继承会多出一个偏移指针(不是虚函数表指针!!!)
* 虚继承会和父类公用一些成员,也就是把父类的东西装在偏移指针内部,调用的时候直接偏移到父类的内存中
*
*/
class A {
public:
int a, b, c;
public:
A() {
cout << "A memory=" << static_cast<void*>(this) << endl;
}
virtual void f() {}
};
class B1 :public A{
public:
B1() {
cout << "B1 memory=" << static_cast<void*>(this) << endl;
}
};
class B2 :virtual public A{
public:
B2() {
cout << "B2 memory=" << static_cast<void*>(this) << endl;
}
};
class C : public B1,public B2{
public:
C(): B1(), B2(){ //调用父类的顺序和这边初始化列表无关 和上面继承顺序有关
cout << "C memory=" << static_cast<void*>(this) << endl;
}
};
int main() {
C c;
cout << sizeof(A) << endl;
cout << sizeof(B1) << endl;
cout << sizeof(B2) << endl;
cout << sizeof(c) << endl;
// c.a = 1;
c.B1::a = 2;
c.B2::a = 3;
// cout << c.a << endl;
return 0;
}
虚继承即在继承的时候加上virtual的修饰符,这个对象前默认开了一个虚继承表,具体用法有以下几种:
- 多继承时保证上一级父类的对象只会生成1个,并且析构的时候会把所有虚继承基类的派生类都析构完了再析构基类
class Person{
public: Person(){ cout<<"Person构造"<<endl; }
~Person(){ cout<<"Person析构"<<endl; }
};
class Teacher : virtual public Person{
public: Teacher(){ cout<<"Teacher构造"<<endl; }
~Teacher(){ out<<"Teacher析构"<<endl; }
};
class Student : virtual public Person{
public: Student(){ cout<<"Student构造"<<endl; }
~Student(){ cout<<"Student析构"<<endl; }
};
class Union : public Teacher, public Student{
public: Union(){ cout<<"TS构造"<<endl; }
~Union(){ cout<<"TS析构"<<endl; }
};
int main()
{
Union a;
return 0;
}
输出:
Person构造
Teacher构造
Student构造
TS构造
TS析构
Student析构
Teacher析构
Person析构
- 除此之外,虚继承还保证了多继承时有相同的祖先其祖先的成员只有一项,其实这和上面说的只有一个对象是一个意思。这个被多个类虚继承的基类也称为虚基类,它的构造是由多继承中第一个继承的类负责构造的,但是对于析构函数如果以下代码中基类的析构函数不进行virtual修饰的话,由于发生了上转型,会多次调用基类的虚构函数,但是基类的对象构造的时候又只有一个,所以程序会崩溃。建议虚基类析构函数都要有virtual修饰
class Person{
public: Person() {name = new char[16];cout<<"Person构造"<<endl;}
virtual ~Person() {delete []name;cout<<"Person析构"<<endl;}
private:
char *name;
};
class Teacher :virtual public Person{
public: Teacher(){ cout<<"Teacher构造"<<endl; }
~Teacher(){ cout<<"Teacher析构"<<endl; }
};
class Student :virtual public Person{
public: Student(){ cout<<"Student构造"<<endl; }
~Student(){ cout<<"Student析构"<<endl; }
};
class Union: public Student,public Teacher{
public: Union(){ cout<<"TS构造"<<endl; }
~Union(){ cout<<"TS析构"<<endl; }
};
int main(int argc,char* argv[])
{
Person *p = new Union(); //发生了上转型
delete p;
return 0;
}
输出:
Person构造
Student构造
Teacher构造
TS构造
TS析构
Teacher析构
Student析构
Person析构
多继承构造顺序
当一个派生对象构造时先去查看有没有虚继承,然后再查有没有实继承,具体流程如下例子:
/**
* 补充:
* 我们知道C++类的静态成员变量是需要初始化的,但为什么要初始化呢。
* 其实这句话“静态成员变量是需要初始化的”是有一定问题的,
* 应该说“静态成员变量需要定义”才是准确的,而不是初始化。
* 两者的区别在于:初始化是赋一个初始值,而定义是分配内存。
* 静态成员变量在类中仅仅是声明,没有定义,所以要在类的外面定义,
* 实际上是给静态成员变量分配内存。
*/
class A {
public:
A() {
cout << static_cast<void*>(this) << endl;
f();
}
A(int a) {
cout << static_cast<void*>(this) << endl;
cout << "aaa" << endl;
}
virtual void f() {
cout << "A()" << endl;
}
};
class B1 :public A{
public :
B1() {
cout << static_cast<void*>(this) << endl;
f();
}
B1(int a) : A(1){
cout << static_cast<void*>(this) << endl;
cout << "bbb" << endl;
}
virtual void f() {
cout << "B1()" << endl;
}
};
class B2 :public A{
public :
B2() {
cout << static_cast<void*>(this) << endl;
f();
}
void f() {
cout << "B2()" << endl;
}
private:
B1 b(); //声明了一个无参函数
static B1 c; //一定要在类外初始化
B1 d;
};
B1 B2::c;
class C :public B1,virtual public B2{ //先b2 再b1
public :
C() {
cout << static_cast<void*>(this) << endl;
cout << "C()" << endl;
}
};
int main() {
// C c(); //声明了一个无参函数c
// C c;
// cout << endl;
// c.f(); // 错误
//若B1、B2虚继承 则C必须覆盖f方法
//若B1、B2普通继承 则C不能够调用f 因为没有覆盖 即使是虚继承也一样
B2 b2;
cout << static_cast<void*>(&b2) << endl;
return 0;
}
/**
* 1,基类的静态定义
* 2,派生类的静态定义
* (上面必须有定义 只是初始化不会调构造)
* 3,基类构造函数
* 4,派生类的成员构造
* 5,派生类的构造函数
*/
多态
以下例子是链型继承时,是否发生多态的区别(具体看上一级有没有那个一模一样方法有没有virtual修饰,如果没有则再往上看,直到链头):
class C {
public:
void toString()
{
cout << "class C" << endl;
}
};
class B : public C {
public:
virtual void toString()
{
cout << "class B" << endl;
}
};
class A : public B {
public:
void toString()
{
cout << "class A" << endl;
}
};
void FunTest1() {
C c;
B b;
A a;
C *pc = &b;
pc->toString(); //这样不算多态
b.toString();
B *pb = &a;
pb->toString(); //多态
}
//CBA
再看个正经多态的例子:
/**
* 当类有虚函数时对象生成时会多一个虚函数表
* 当有虚继承时会有虚继承表
* 虚函数是动态联编的,即运行的时候再去查虚函数表,而不是编译时绑定好的函数。
* 因此,虚函数的调用会带来额外的空间开销和时间开销
*/
class Base {
public:
virtual void fun()
{
cout << "Base::fun()" << endl;
}
};
class Derived : public Base {
public:
virtual void fun()
{
cout << "Derived::fun()" << endl;
}
};
void FunTest()
{
Derived d;
Base* pb = &d;
pb->fun();//基类指针pb指向子类,可以打印Derived::fun()
Base& rb = d;
rb.fun();//基类对象pb引用子类,可以打印Derived::fun()
}
菱形继承
直接上例子,
- 其中C由于实继承了几个一模一样的方法,调用的时候必须要加上域前缀,否则会产生二义性;
- D和E虽然有虚继承了B1、B2,但不解决根本的A对象创建多次的问题,因为B1、B2没有虚继承A;
- 而E2、E3由于B3、B4都是虚继承A的缘故,所以虚基类A的对象只会开辟一次,但这两个的区别是如果后面还有继承他们的话E2会产生两个B3,B4,而E3不会,E3相当于让B3,B4也成了虚基类
具体代码如下:
/**
* 虚继承:
* 用来解决“菱形”继承出现的许多相同名称的成员
*/
class A {
public: //private对字节的计算无影响
int a, b;
static int c;
public:
A() {
cout << "A memory=" << static_cast<void*>(this) << endl;
}
};
class B1 : public A{
public:
B1() {
cout << "B1 memory=" << static_cast<void*>(this) << endl;
}
};
class B2 : public A{
public:
B2() {
cout << "B2 memory=" << static_cast<void*>(this) << endl;
}
};
class C : public B1, public B2{
public:
C(): B2(), B1(){ //调用父类的顺序和这边初始化列表无关 和上面继承顺序有关
cout << "C memory=" << static_cast<void*>(this) << endl;
}
};
class D : virtual public B1, public B2{
public:
D(): B2(), B1(){ //调用父类的顺序和这边初始化列表无关 和上面继承顺序有关
cout << "D memory=" << static_cast<void*>(this) << endl;
}
};
class E : virtual public B1,virtual public B2{
public:
E(): B2(), B1(){ //调用父类的顺序和这边初始化列表无关 和上面继承顺序有关
cout << "E memory=" << static_cast<void*>(this) << endl;
}
};
class B3 :virtual public A{
public:
B3() {
cout << "B3 memory=" << static_cast<void*>(this) << endl;
}
};
class B4 :virtual public A{
public:
B4() {
cout << "B4 memory=" << static_cast<void*>(this) << endl;
}
};
class E2 : virtual public B3,virtual public B4{
public:
E2(): B4(), B3(){ //调用父类的顺序和这边初始化列表无关 和上面继承顺序有关
cout << "E2 memory=" << static_cast<void*>(this) << endl;
}
};
class E3 : public B3,public B4{
public:
E3(): B4(), B3(){ //调用父类的顺序和这边初始化列表无关 和上面继承顺序有关
cout << "E3 memory=" << static_cast<void*>(this) << endl;
}
};
template<typename T> //查看大小
void count_mem(T& t) {
cout << "memory=" << const_cast<T*>(&t) << " size=" << sizeof(t) << endl;
}
int A::c = 100;
int main() {
A a; //8byte
count_mem(a); //static成员内存不算在对象内
cout << endl;
B1 b1; //8byte
count_mem(b1); //虽然private不可见 但是还是有这块内存
cout << endl;
B2 b2; //8byte
count_mem(b2); //虽然private不可见 但是还是有这块内存
cout << endl;
C c; //16byte
count_mem(c); //两个叠加的内存 初始化调用按照列表继承的顺序执行
cout << endl;
D d; //24byte(vscode g++编译器 下同) dev 20byte
count_mem(d);
cout << endl;
E e; //24byte
count_mem(e);
cout << endl;
//加了虚继承反而字节增多?
//中间层的父类也是虚继承也会这样:
E2 e2; //24 20
count_mem(e2);
cout << endl;
E3 e3; //24 20
count_mem(e3);
cout << endl;
// c.x = 1; //编译错误 存在二义性
c.B1::a = 1; //正确1
C::B1::c = 1; //静态的访问方法1
c.B1::c = 1; //静态的访问方法2
c.c = 1; //静态的访问方法3
E::B1::c = 2; //ok
e.B1::c = 1; //ok
e.c = 1; //ok
E2::B3::c = 1; //ok
e2.B3::c = 1; //ok
e2.c = 1; //ok
E3::B3::c = 1; //ok
e3.B3::c = 1; //ok
e3.c = 1; //ok
return 0;
}
菱形继承中的静态成员分析
例子:
class A {
public: //private对字节的计算无影响
int a, b;
static int c;
public:
A() {
cout << "A memory=" << static_cast<void*>(this) << endl;
}
};
class B1 : public A{
public:
B1() {
cout << "B1 memory=" << static_cast<void*>(this) << endl;
}
};
class B2 :public A{
public:
B2() {
cout << "B2 memory=" << static_cast<void*>(this) << endl;
}
};
class E : virtual public B1,virtual public B2{
public:
E(): B1(), B2(){ //调用父类的顺序和这边初始化列表无关 和上面继承顺序有关
cout << "E3 memory=" << static_cast<void*>(this) << endl;
}
};
template<typename T> //查看大小
void count_mem(T& t) {
cout << "memory=" << const_cast<T*>(&t) << " size=" << sizeof(t) << endl;
}
int A::c = 1; //关键!!!
int main() {
A a;
count_mem(a);
B1 b1;
count_mem(b1);
B2 b2;
count_mem(b2);
E e;
count_mem(e);
a.c = 1;
return 0;
}
额外的注意事项
- 一定要注意const方法和java的final方法不一样,java中是不允许final不允许重写,也就是覆盖。而const方法只要后面和它一模一样,子类中会发生两件事情:如果父类这个方法有修饰virtual,则子类就是覆盖;否则就是重定义,即隐藏。
- 注意一个函数修饰了const和没有修饰const是两个函数。对于普通类,在同一个域出现这两个就是重载,在父子类中出现这两个(无论父类const子类非const还是反过来)就是重定义。对于抽象类,在其的子类就也是抽象的,因为没实现父类的方法。