文章目录
1、类的组合
在一个类中包含其他类的对象
如果组合类的构造函数带参数,在本类的构造函数中使用参数列表初始化进行赋值。
C(int a, int b, int c):a(a), b(b),dataC©{cout<<“C”<<endl;}
类的构造函数调用顺序:先调用内嵌对象的构造函数(有多个内嵌对象时按声明顺序),再调用本类构造函数。
析构函数和构造函数调用顺序相反。
2、继承和派生
保持已有类的特性而构造新类的过程为继承
在已有类的基础上新增自己类的特性而产生的新类的过程为派生。
基类:已有的类称为基类,也称父类
派生类:从基类派生出来的类,也称子类
派生类的声明:
class 类名 : 继承方式 基类名
如:
class B:public A; //B是从A继承而来,或者说A派生出B
{
};
派生类继承了基类所有方法,除以下情况外:
(1)基类的构造、析构、拷贝构造、赋值函数
(2)基类的重载运算符
(3)基类的友元函数
3、继承与派生的目的
继承的目的:实现代码重用
派生的目的:当出现新的问题或新的需求,在基类中无法解决时,需要对原有的类进行改造,形成新的类。
4、三种继承方式
有三种继承方式:公有继承、私有继承、保护继承。如果没有显示给出哪种继承方式,默认是私有继承,如:
class A:B; //默认是私有继承
struct A:B; //默认是公有继承
不同继承方式的影响主要体现在:
派生类成员对基类成员的访问权限
派生类对象对基类成员的访问权限
(1)公有继承:
基类的public和protected成员的访问属性在派生类中保持不变,也就是说原本在基类中的public和protected成员继承之后,在派生类中还是public和protected成员,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问从基类继承而来的public和protected成员,但不能直接访问从基类继承而来的private成员。
派生类的对象只能访问从基类继承而来的public成员。
#include <iostream>
#include <cstring>
using namespace std;
class A
{
public:
A(int a1,int a2)
{
this->a1 = a1;
this->a2 = a2;
}
void funA()
{
cout << "a1 = " << a1 << endl;
}
private:
int a1;
protected:
int a2;
};
class B:public A
{
public:
B(int a1,int a2,int b1,int b2):A(a1,a2)
{
this->b1 = b1;
this->b2 = b2;
}
void funB()
{
//不能直接访问a1,因为它是基类中的私有成员
//cout << "a1 = " << a1 << endl;
cout << "a2 = " << a2 << endl;
funA();
}
//void funA(); //基类的公有成员
private:
int b1;
//int a1; //基类的私有成员(不可见),并不是变成派生类的私有成员
protected:
int b2;
//int a2; //基类的保护成员
};
int main(void)
{
B b(10,20,100,200);
b.funB();
b.funA();
}
如果在基类中有带无默认参数的构造函数,派生类中也要写带参数的构造函数,并且要给基类的构造函数传参,如:
class A
{
A(int a):a(a)
{
}
private:
int a;
}
class B:public A
{
B(int a,int b):b(b),A(a)
{
}
private:
int b;
}
(2)私有继承:
基类的public和protected成员都以private身份出现在派生类中,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问从基类继承而来的public和protected成员,但不能直接访问从基类继承而来的private成员。
通过派生类的对象不能直接访问从基类继承而来的任何成员。
#include <iostream>
#include <cstring>
using namespace std;
class A
{
public:
A(int a1,int a2)
{
this->a1 = a1;
this->a2 = a2;
}
void funA()
{
cout << "a1 = " << a1 << endl;
}
private:
int a1;
protected:
int a2;
};
class B:private A
{
public:
B(int a1,int a2,int b1,int b2):A(a1,a2)
{
this->b1 = b1;
this->b2 = b2;
}
void funB()
{
//不能直接访问a1,因为它是基类中的私有成员
//cout << "a1 = " << a1 << endl;
cout << "a2 = " << a2 << endl;
funA();
}
private:
int b1;
//int a1; //基类的私有成员(不可见),并不是变成派生类的私有成员
//void funA(); //基类的公有成员
//int a2; //基类的保护成员
protected:
int b2;
};
int main(void)
{
B b(10,20,100,200);
b.funB();
//b.funA(); //不能访问
}
(3)保护继承
基类的public和protected成员都以protected身份出现在派生类中,但基类的private成员不可直接访问。
派生类中的成员函数可以直接访问从基类继承而来的public和protected成员,但不能直接访问从基类继承而来的private成员。
通过派生类的对象不能直接访问从基类继承而来的任何成员
#include <iostream>
#include <cstring>
using namespace std;
class A
{
public:
A(int a1,int a2)
{
this->a1 = a1;
this->a2 = a2;
}
void funA()
{
cout << "a1 = " << a1 << endl;
}
private:
int a1;
protected:
int a2;
};
class B:protected A
{
public:
B(int a1,int a2,int b1,int b2):A(a1,a2)
{
this->b1 = b1;
this->b2 = b2;
}
void funB()
{
//不能直接访问a1,因为它是基类中的私有成员
//cout << "a1 = " << a1 << endl;
cout << "a2 = " << a2 << endl;
funA();
}
private:
int b1;
//int a1; //基类的私有成员(不可见),并不是变成派生类的私有成员
protected:
int b2;
//void funA(); //基类的公有成员
//int a2; //基类的保护成员
};
int main(void)
{
B b(10,20,100,200);
b.funB();
//b.funA(); //不能访问
}
继承方式 | 基类的公有成员 | 基类的私有成员 | 基类的保护成员 |
---|---|---|---|
公有继承 | 派生类的公有成员 | 不可见 | 派生类的保护成员 |
私有继承 | 派生类的私有成员 | 不可见 | 派生类的私有成员 |
保护继承 | 派生类的保护成员 | 不可见 | 派生类的保护成员 |
继承中的指针问题:
(1)基类指针可以指向派生类对象,但此类指针只能调用从基类继承而来的函数,不能调用派生类新增的函数。是实现多态的必要条件
A *a = new B; //基类A的指针指向派生类B的对象
(2)派生类指针不能指向基类对象
总结:
1.private成员只能被本类成员(类内)和友元访问,不能被派生类访问;
2.protected成员可以被派生类访问。
1.类的一个特征就是封装,public和private作用就是实现这一目的。所以:
用户代码(类外)可以访问public成员而不能访问private成员;private成员只能由类成员(类内)和友元访问。
2.类的另一个特征就是继承,protected的作用就是实现这一目的。所以:
protected成员可以被派生类对象访问,不能被用户代码(类外)访问。
4、C++中struct的拓展
C++中对struct进行了扩充
struct可以包含成员数据,也可以包含成员函数,还可以继承
struct和class的联系与区别:
联系:
它们可以相互替换,相互继承
区别:
(1)最本质的区别:
class默认继承方式是私有继承,struct默认是公有继承
(2)还有一个区别:
class可以定义模板,但struct不可以
如果想体现出类与对象的话,就用class,如果想体现出数据结构,就用struct
5、多继承与多继承中的构造函数
单继承:一个派生类只有一个基类
多继承:一个派生类继承于多个基类
多层继承:派生类的基类还有基类
多重派生:一个基类派生多个子类
多继承中派生类的构造函数声明:
//派生类
class C:public A,private B
{
public:
//派生类构造函数要调用基类的构造函数
C(int a,int b,int c):A(a),B(b)
{
this->c = c;
}
private:
int c;
};
在多继承中,产生的二义性:
(1)同名二义性(倒三角问题)
解决方法:通过“类名::”进行限定
c.A::fun(); //指定调用A中的fun
c.B::fun(); //指定调用B中的fun
(2)路径二义性(菱形问题)在多重派生和多继承时一定会出现命名冲突的问题。
虚拟继承
class B:virtual public A //A是虚基类
class C:virtual public A
6、虚继承和虚基类
虚继承的目的是为了解决多继承产生的路径二义性问题
将共同基类设为虚基类,这时从不同路径继承而来的相同的数据和函数在内存中只有一个拷贝,不仅解决了二义性问题,还节省了空间。
虚基类的声明方法:
class 派生类名: virtual 继承方式 基类名
如:
class B:virtual public A //A是虚基类
class C:virtual public A
说明:virtual 是关键字,说明该基类是虚基类
虚继承中对虚基类初始化发生冲突的问题:
B会初始化A,C也会初始化A,此时,编译器无法判断到底用哪个去初始化A。
解决方式:
用D去初始化A,B和C就不会再去初始化A了。
D(int a,int b,int c,int d):B(a,b),C(a,c),A(10)
{
this->d = d;
}
路径二义性(菱形问题)test.cpp / 32位机
#include <iostream>
using namespace std;
class A
{
public:
A(){cout<<"A"<<endl;}
~A(){cout<<"~A"<<endl;}
int data; //4
};
//多派生,从A派生B、C
/*----------------------------------------------*/
class B:virtual public A//父类 int data 4 虚基类表指针4
{
public:
B(){cout<<"B"<<endl;}
~B(){cout<<"~B"<<endl;}
};
class C:virtual public A//父类 int data 4 虚基类表指针4
{
public:
C(){cout<<"C"<<endl;}
~C(){cout<<"~C"<<endl;}
};
/*----------------------------------------------*/
class D:public B,public C //多继承 4 + 4 + 4(两个B、C虚基类表指针+int data)
{
public:
D(){cout<<"D"<<endl;}
~D(){cout<<"~D"<<endl;}
};
int main()
{
A a;
B b;
D d;
cout<<sizeof(b)<<endl; //8
cout<<sizeof(d)<<endl; //12
cout << "Hello World!" << endl;
return 0;
}
通过在Qt中调试c++,可以看到d继承了B、C的int(来自A)和虚基类表指针,在d中可以看到B、C中继承的基类A为同一个空间,它通过虚基类表指针维护,节省了内存,避免了数据不一致的问题。
B和C类是虚继承于A类,A类是B、C的虚基类(在使用虚继承时,会在对象内存空间的最开头建立一个虚基类表),只要使用虚继承那么就会将虚基类记录到表中,后面如果还有虚继承,会进行查表,表中有该基类就不在创建。
7、构造函数和析构函数的调用顺序问题
构造函数原则:
1、当基类中声明有默认形式的构造函数或未声明构造函数时,派生类构造函数可以不向基类构造函数传递参数。
2、若基类中未声明构造函数,派生类中也可以不声明,全采用默认形式构造函数。
3、当基类声明有带形参的构造函数时,派生类也应声明带形参的构造函数,并将参数传递给基类构造函数。
总结:
基类没写构造函数,派生类可以写可以不写,基类写了构造函数,并且没有带默认参数,派生类必须写构造函数,并且向基类构造函数传参。
多继承的构造函数声明:
派生类名(基类1形参,基类2形参,…基类n形参,本类形参):基类名1(参数), 基类名2(参数), …基类名n(参数),对象数据成员的初始化
{
本类成员初始化赋值语句;
};
class D:public A,private B,protected C
{
public:
//多继承且含内嵌对象时的构造函数
D(int a,int b,int c,int d):A(a),a(a),B(b),C(c),b(b),c(c)
{
this->d = d;
}
private:
int d;
A a;
B b;
C c;
};
构造函数的调用顺序:
1、基类的构造函数,调用顺序是按照继承时声明的顺序(从左到右)
2、基类对象的构造函数,调用顺序是按照在类中声明的顺序(从上到下)
3、派生类的构造函数
析构函数的调用顺序:
与构造函数调用顺序相反
8、C++中的类型转换
1、类的类型转换
类是一种比较复杂的数据类型,可以完成数据类型的转变。
必须发生在有继承关系的类对象之间。
向上转型:有子类转换为父类
子类对象赋值给父类对象
子类指针赋值给父类指针
子类引用赋值给父类引用
在向上转型时,父类的对象、指针、引用会丢失子类对象中的成员,本质是将派生类对象中的数据填充到父类对象中,多余的部分丢弃,所以是类型安全的。
向下转型:由父类转换为子类
父类对象赋值给子类对象
父类指针赋值给子类指针
父类引用赋值到子类引用
不安全
2、静态转换
static_cast <目标类型>原始数据
进行基础数据类型的转换
父子类型之间的转换
没有父子关系的类指针转换会失败
3、动态转换
dynamic_cast<目标类型>(原始数据)
一般用于指针转换
不能用于基本数据类型转换
动态转换在进行是会检查类型,比static_cast安全
4、常量转换
cosnt_cast<目标类型>(原始数据)
修改目标的const属性
test.cpp
#include <iostream>
using namespace std;
class A{
public:
int data;
A(int a):data(a){cout<<"A"<<endl;}
~A(){cout<<"~A"<<endl;}
A(const A &a){
data = a.data;
cout<<"A &a"<<endl;
}
};
class B:public A{
public:
int data2;
B(int b):A(b){cout<<"B"<<endl;}
~B(){cout<<"~B"<<endl;}
B(const B &b):A(b)
{
data2 = b.data2;
cout<<"B &b"<<endl;
}
};
void fun()
{
char a = '0';
double numd = static_cast<double>(a);
cout<<numd<<endl;
B b1(20);
A a1 = static_cast<A>(b1);
B *b2 = new B(20);
A *a2 = dynamic_cast<A*>(b2);
const int tmp_n = 10;
const int *tmp = &tmp_n;
//int *t = tmp;//t和tmp的类型不相同,不能直接赋值
int *t = const_cast<int *>(tmp);
cout<<"---------------------------------"<<endl;
cout<<*t<<endl;
cout<<*tmp<<endl;
cout<<"---------------------------------"<<endl;
cout<<t<<endl;
cout<<tmp<<endl;
cout<<"---------------------------------"<<endl;
*t = 20;
cout<<tmp_n<<endl; //10
cout<<*t<<endl; //20
cout<<*tmp<<endl; //20
cout<<"---------------------------------"<<endl;
const int num = 10;
int& tmp_num = const_cast<int &>(num);
cout<<"---------------------------------"<<endl;
cout<<&num<<endl;
cout<<&tmp_num<<endl;
cout<<num<<endl;
cout<<tmp_num<<endl;
cout<<"---------------------------------"<<endl;
tmp_num = 20;
cout<<num<<endl; //10
cout<<tmp_num<<endl; //20
cout<<&num<<endl;
cout<<&tmp_num<<endl;
cout<<"---------------------------------"<<endl;
}
int main()
{
//向上转型
B b1(10);
A a1 = b1;//向上转型,将子类对象赋值给父类对象
B *b2 = new B(20);
A *a2 = b2;//向上转型,将子类指针赋值给父类指针
B &b3 = b1;
A &a3 = b3;//向上转型,将子类引用赋值给父类引用
//向下转型 不安全
// A a4(10);
// B b4 = a4;//向下转型,将父类对象赋值给子类对象 子类对象空间更大,父类不能完全赋值,不安全
// A *a5 = new A(10);
// B *b5 = a5;//向下转型,将父类指针赋值给子类指针 子类指针访问范围更大,不安全
// A &a6 = a4;
// B &b6 = a6;
fun();
cout << "Hello World!" << endl;
return 0;
}