C++的三大特性:封装、继承、多态
封装:为了隐藏机密的数据
继承:为了代码复用,函数重写,根据需求增加更多的功能
多态:为了在相同的调用根据实际情况得到不同的结果
1)隐式类型转换(bug的重要来源):
C语言支持 小类型->大类型(安全,不会发生数据截断/丢失)
char(1个字节) ->short->int(4个字节)->unsigned int-> long->unsigned long->float->double
==>编译器隐式转换类型带来的bug案例
```cpp
```cpp
short s = 'a';
unsigned int ui = 1000;
int i = -2000;
if( (ui + i) > 0 ) //-2000+1000= -1000 按道理应该输出Negative,但是却输出了这个
{
cout << "Positive" << endl; //理由是编译器做了隐式类型转换int ->unsigned int 型,无符号总是大于0
}
else
{
cout << "Negative" << endl;
}
//理想是输出2,但编译器优化(觉得int比较高效) short-> int ,char->int
cout << "sizeof(s + 'b') = " << sizeof(s + 'b') << endl; //
问题:
普通类型能否->类类型
类类型能否->普通类型
转换构造函数{构造函数只有一个参数,参数类型未非自身类型(class)}
来源:编译器会偷偷调用这个函数进行转换
explicit Test(int i)
{
mValue = i;
}
int main
{ Test t ;
t = 5;
t= t+10;
}
explicit 拒绝编译器自行转换
2)显示转换方式:
**2.1)整形->类对象;**static_cast< className>(value)
static_cast< className>(value) //C++推荐写法
(ClassName) value => 不推荐,C语言写法
ClassName(value) ==>不推荐,调用构造函数的写法
class Test
{
int mValue;
public:
Test()
{
mValue = 0;
}
explicit Test(int i) //这个又叫转换构造函数{只有一个参数,参数类型未非自身类型(class)}
{
mValue = i;
}
Test operator + (const Test& p)
{
Test ret(mValue + p.mValue);
return ret;
}
int value()
{
return mValue;
}
};
int main()
{
Test t;
t = 5; //当有转换构造函数 Test(int i)时,编译器会自动进行隐式转换=>t= Test(5)
//当加上explicit后,拒绝编译器自行进行隐式转换,只能采用显示转换
t = static_cast<Test>(5); // t = Test(5);
Test r
r = t + static_cast<Test>(10); // r = t + Test(10);
cout << r.value() << endl;
return 0;
}
2.2)类类型转化为其他类型(包括其他): 类型转换函数 (operator type) (工程上很少使用这个函数(无法抑制编译器自行转换),通常使用成员函数 operator .tovalue)
1)当只存在 operator Value()类型转换函数时,编译器隐式转换 v=t-=> value v=t.operator value() ,
2)当只存在 Value(Test& t) 构造转换函数时,编译器隐式转换为 v=t- =>value =value(t);
3) 但当两个都存在时,由于同一等级,编译器会犯难
4)为了抑制编译器的隐式转换,会在转换函数里面加explicit Value(Test& t)
5)而类型转换函数由于无法抑制隐式转换,所以工程中基本不用类型转换函数,而是自定义一个类的成员函数并命名为 Value totype()函数替换
=>value v = t.value()
或 value = ??类类型转化无关系继承****
class Value
{
public:
Value()
{
}
explicit Value(Test& t) //在赋值符号左边边,转换类型有转换构造函数
{ // v=t => value =value(t);
}
};
class Test
{
int mValue;
public:
Test(int i = 0)
{
mValue = i;
}
int value()
{
return mValue;
}
operator Value() //在赋值符号右边,被转换类型如果有类型转换函数
//v=t =>value v=t.operator value()
{
Value ret;
cout << "operator Value()" << endl;
return ret;
}
};
int main()
{
Test t(100);
Value v = t;
/*当只存在 operator Value()类型转换函数时,编译器隐式转换 value v=t.operator value() ,
//当只存在 Value(Test& t) 构造转换函数时,编译器隐式转换为 value =value(t);
但当两个都存在时,由于同一等级,编译器会犯难
*为了抑制编译器的隐式转换,会在转换函数里面加explicit Value(Test& t)
*而类型转换函数由于无法抑制隐式转换,所以工程中基本不用类型转换函数,而是自定义一个类的成员函数并命名为 Value totype()函数替换
value v = t.value()
或 value = ??类类型转化无关系继承
*/
return 0;
}
3)继承 ==>(主要讨论父子间继承和冲突关系)
(意义:代码复用的手段,子类继承父类的所有功能,并且可以在子类中重写已有功能或添加新功能)
1)工程项目一般只用public继承,其他private 跟protected的继承太过复杂
2)C++的派生语言优化了,只支持public继承(Java C# d语言),3种访问级别(public protected private)[protected: 继承的子类可以访问父类的成员,但外部无法访问protected成员]
3)继承间的构造函数与析构函数
子类构造函数—必须初始化继承而来的成员
初始化父类对象的两个方式:
通过初始化列表或者直接赋值()==》父类的private对象无法直接访问 ==》还是得用父类的构造函数
子类构造时调用父类的构造函数进行初始化
问:父类的构造函数在子类怎么调用?
隐式调用(只用于无参列表,或使用默认参数的构造函数)
显式调用(通过初始化列表进行调用),
4) {构造顺序:先父母,再成员,后自己},析构顺序刚好相反
5)父子间冲突
5.1)子类的成员可以隐藏父类的成员
5.2)子类的函数也可以隐藏父类的成员函数(由此无法重载,子类可以定义完全一样的父类函数)
5.3)当被隐藏后,想要调用父类同名的成员/成员函数,需要利用作用域分辨符
调用父类的成员(c(子).Parent(父)::mi)
调用父类的成员函数:c.Parent::add(2, 3);
#include <iostream>
#include <string>
using namespace std;
class Object
{
string ms;
public:
Object(string s)
{
cout << "Object(string s) : " << s << endl;
ms = s;
}
~Object()
{
cout << "~Object() : " << ms << endl;
}
};
class Parent : public Object //在类名这里 : 表示继承 publice 表示继承方式,工程上都用public继承
{
string ms;
public:
Parent() : Object("Default") // 这里的:是初始化列表,这里是显式调用,当创建Parent对象时
{ //会先创建父类,调用Object的构造函数
cout << "Parent()" << endl;
ms = "Default";
}
Parent(string s) : Object(s) //这里如果没有Object(s),会隐式调用父类Object()
{
cout << "Parent(string s) : " << s << endl;
ms = s;
}
~Parent()
{
cout << "~Parent() : " << ms << endl;
}
};
//构造顺序,先父母,后客人,再自己
class Child : public Parent //两种关系 ,继承关系:继承了Parent
{
Object mO1; //组合关系,Object类的成员对象,由于无'后客人的体现',所以要用初始化列表
Object mO2;
string ms;
public:
Child() : mO1("Default 1"), mO2("Default 2") //由于无'后客人的体现',所以要用初始化列表,这里有一个隐式调用父类构造函数, Parent()
{
cout << "Child()" << endl;
ms = "Default";
}
Child(string s) : Parent(s), mO1(s + " 1"), mO2(s + " 2")
{ //初始化列表,这里的初始化顺序 mO1 mO2 Parent(s)==>这个是利用初始化列表显示调用 Parent(string s),
//如果没有这个Parent(s),则会隐式调用 Parent()
cout << "Child(string s) : " << s << endl;
ms = s;
}
~Child()
{
cout << "~Child() " << ms << endl;
}
};
int main()
{
Child cc("cc");
/*输出
Object(string s) : cc
Parent(string s) : cc
Object(string s) : cc 1
Object(string s) : cc 2
Child(string s) : cc
~Child() cc
~Object() : cc 2
~Object() : cc 1
~Parent() : cc
~Object() : cc
*/
cout << endl;
return 0;
}
同名覆盖:
4)父子间同名覆盖问题–》来源:赋值兼容原则
赋值兼容性的定义:
子类对象可以当做父类对象使用
==》子类对象可以直接赋值父类对象
==》子类对象可以直接初始化父类对象
==》父类指针可以直接指向子类对对象
==》父类引用可以直接引用子类对象
**问题1)编译器规则/赋值兼容原则(当采用父类指针指向/引用子类对对象时,子类对象退化为父类对象)
==》带来的后果:无法访问子类的成员
Parent p1(c); //子类对象c直接初始化父类对象
4
Parent& rp = c; //父类指针引用子类对象,用rp做c的别名 ==》子类对象已经退化为父类成员
Parent* pp = &c; //父类指针可以指向子类对象 ==》子类对象已经退化为父类成员
rp.mi = 100;
rp.add(5); // 没有发生同名覆盖?
//==》虽然rp 已经代表了c对象, 但c子类对象已经退化为父类成员,add(5)正是父类的成员函数
rp.add(10, 10); // 没有发生同名覆盖?
/* 为什么编译不过? */
// pp->mv = 1000;
// pp->add(1, 10, 100); //
//==》虽然pp 已经获得了c对象的地址, 但子类对象c已经退化为父类成员,add(1, 10, 100)正是子类的成员函数,由此无法访问
return 0;
问题2)函数重写遇上赋值兼容??
定义:子类对象重写父类对象的成员函数(同名覆盖的情况子类可以同名父类的成员和成员函数),且重定义发生在继承关系
为什么要函数重写?父类的功能不足以满足,所以才需要重写,在下面的案例中,重写了printf的函数,但输出确是父类的打印??
原因:编译器在编译过程中做了选择,how_to_print函数完整,在编译阶段,编译器并不知道指针p指向了哪个对象,根据兼容性原则(子类对象可以当做父类对象来使用),编译器选择最安全的策略,一律看做是父类对象 ==》由此也引出了virtual 多态的概念
void how_to_print(Parent* p)
{ //根据赋值兼容原则,作为付磊磊
p->print();
}
int main()
{
Parent p;
Child c;
how_to_print(&p); // 期望输出 to print: I'm Parent.
how_to_print(&c); // 期望输出:I'm Child. 实际输出:I'm Parent
return 0;
}
**
5.1)多态(特性):virtual (虚函数) =>相同的调用语句,产生不同的调用结果
意义:根据实际运行来判断如何调用重写函数(在程序运行中呈现动态特性)
需求:当触发兼容性原则时(当采用父类指针指向/引用子类对对象时,子类对象退化为父类对象),仍可以根据实际需求,选择采用父类/子类对象===》函数重写必须多态实现,否则没意义
关键字:virtual 在父类被重写的成员函数声明 >虚函数》编译器会知道
class Parent
{
virtual void print()
{
cout << "I'm Parent." << endl;
}
}
class Child : public Parent
{
public:
void print()
}
5.2)多态的本质
父类对象virtual 定义的虚函数存放在虚函数表(存储成员函数地址的数据结构)中,编译器生成和维护,那存放在哪里?=》虚函数表是全局变量,且全局唯一 =》存放在全局数据区
怎么实现特性多态=》当该父类的成员函数定义为虚函数时,那么每个对象都就隐藏了指向虚函数表的指针(大小为4个字节)=》一个编译器知道该函数为虚函数时=》先去虚函数表里查询有无该函数的地址,如果无则直接确认add的地址
劣势=》多了查询,虚函数的效率 < 普通成员函数
链接地址:https://blog.youkuaiyun.com/jiary5201314/article/details/52627630
在c中定义(模拟c++虚函数表)
struct VTable //全局变量,全局唯一,存放在数据区
{
void * (pAddr)(void*,int); //void* 的理由 c++是面向对象,多态是既可以是子类的成员函数,也可以是指向父类的成员函数
}
//5.3)函数重载与函数重写
函数重载:在同一作用域,函数名一样,参数列表、个数不同,属于静态联编(在编译期间就能确定具体函数调用)
函数重写:函数名跟参数列表一致,但发生在继承关系,子类重写父类的成员函数,属于动态联编(在程序实际运行才能知道具体的函数调用)
void run(Parent* p)
{
p->func(1, 2); // 展现多态的特性
// 动态联编
}
class Parent //这个类期望被继承
{
public:
virtual void func()
{
cout << "void func()" << endl;
}
virtual void func(int i)
{
cout << "void func(int i) : " << i << endl;
}
virtual void func(int i, int j)
{
cout << "void func(int i, int j) : " << "(" << i << ", " << j << ")" << endl;
}
};
class Child : public Parent
{
public:
void func(int i, int j) //这个重写了父类的 virtual void func(int i, int j)
{
cout << "void func(int i, int j) : " << i + j << endl;
}
void func(int i, int j, int k) //同名覆盖(发生在父类和子类之间),会覆盖void func() void func(int i) void func(int i, int j)
{ // void func(int i, int j, int k) 与 void func(int i, int j) 的关系:函数重写
cout << "void func(int i, int j, int k) : " << i + j + k << endl;
}
};
int main()
{
Parent p;
p.func(); // 静态联编
p.func(1); // 静态联编
p.func(1, 2); // 静态联编
cout << endl;
Child c;
c.func(1, 2); // 静态联编 //辨认参数 func(int i, int j)
cout << endl;
run(&p);
run(&c);
return 0;
}
6.1)抽象类(纯虚函数)与接口
抽象类的概念:为反映实际中的抽象概念,如图形
抽象类的实现: 1)C++语言上并没有提供抽象类,只能用纯虚函数实现抽象类,带有纯虚函数的类叫抽象类
抽象类的特点:
1)抽象类不能产生对象(没有必要产生对象,如图形),只能被继承;
2)纯虚函数(是指定义函数原型的成员函数)不需要写具体实现
3)某一些函数没有具体实现
4)子类须重写纯虚函数,重写完为虚函数,如果不重新纯虚函数,则为子类为抽象类,被下一个子类重新功能
class Shape
{
public:
virtual double area() = 0; //纯虚函数的写法 =0,告诉编译器是纯虚函数,没有具体实现
};
class Rect : public Shape
{
int ma;
int mb;
public:
Rect(int a, int b)
{
ma = a;
mb = b;
}
double area() //类须重写纯虚函数,重写完为虚函数(多态的工能),如果不重新纯虚函数,则为子类为抽象类,被下一个子类重新功能
{
return ma * mb;
}
};
class Circle : public Shape
{
int mr;
public:
Circle(int r)
{
mr = r;
}
double area()
{
return 3.14 * mr * mr;
}
};
void area(Shape* p) //多态试验,根据实现运行呈现不同效果
{
double r = p->area();
cout << "r = " << r << endl;
}
int main()
{
Rect rect(1, 2);
Circle circle(10);
area(&rect);
area(&circle);
return 0;
6.2)接口(特殊抽象类模拟) =》一组行为的规范 //可以看作是一组函数原型
C++没有接口的概念,但后续的Java C#中存在接口的
定义:
1)没有成员函数
2)成员变量全部为publice属性
3)成员函数均为纯虚函数
例子:网络编程的收发 :以太网
串口编程(从串口收发) :
蓝牙编程(通过蓝牙收发):
USB编程(通过USB口收发):
共性:都是收发:
不同点:介质不一样
class Channel //抽象的概念,抽象类的存在 ,4个操作
{ //1)无成员函数
public: 2)
virtual bool open() = 0;
virtual void close() = 0;
virtual bool send(char* buf, int len) = 0;
virtual int receive(char* buf, int len) = 0;
};
实际作用? QT 项目合成
7)被遗弃的多重继承
问题1?子类可以拥有多个父类吗?
答c++ 支持编写多重继承的代码
多重继承的特性:
a)一个子类可以拥有多个父类
b)子类拥有所有父类成员
c)子类继承所有父类的成员函数
4)子类对象可以当作任意对象使用(赋值兼容性原则也适用)
语法:
class Derived : public BaseA, public BaseB
{
}
7.2)多重继承带来的问题:
a)通过多重继承来的对象可能拥有“不同的地址”==》引出问题:指向不同地址值的两个指针可能是指向同一个对象(以往指向同一个对象,两个指针的地址是一样的)
解决方案:无
Derived d(1, 2, 3);
BaseA* pa = &d;
BaseB* pb = &d;
输出:
pa = 0x7fff70575fa0
pb = 0x7fff70575fa4
b)多重继承可能产生冗杂的成员
解决方案:需继承,中间层父类不再关心顶层父类的初始化(不调用父类的构造函数),最终子类必须调用顶层父类的构造函数 =》增加了复杂性
但虚继承也会带来的问题:架构设计可能出现问题–>架构设计需要继承时,无法确定是直接继承还是需继承(当今类的层次 有 四 五 六层,花费了项目管理等时间问题)
public:
People(string name, int age)
{
m_name = name;
m_age = age;
}
void print()
{
cout << "Name = " << m_name << ", "
<< "Age = " << m_age << endl;
}
};
class Teacher : virtual public People
{
public:
Teacher(string name, int age) : People(name, age)
{
}
};
class Student : virtual public People
{
public:
Student(string name, int age) : People(name, age)
{
}
};
class Doctor : public Teacher, public Student
{
public:
Doctor(string name, int age) : Teacher(name+“1”, age+1), Student(name+“2”, age+2), People(name, age)
{
}
};
int main()
{
Doctor d("Delphi", 33);
d.print(); //这样写编译器会保错,说不知道应该使用哪个,除非加了虚继承class Teacher : virtual public People ,家里之后就只有一个printf
d.Teacher::print();
d.Studemt::print();
return 0;
输出:
Name = Delphi1,Age =34
Name = Delphi2,Age =35
}
c)多重继承可能产生多个虚函数表==>造成不可思议的问题,且难排除(原因:通过多重继承来的对象可能拥有“不同的地址”,funA跟funB为虚函数,要靠虚函数指针,在虚函数找对应地址)
解决方案:与多继承i相关的强制类型转换用dynamic_cast(进行强制类转换)
BaseA* pa = &d;
BaseB* pb = &d;
BaseB* pbb = (BaseB*)pa; //出错,打印了"BaseA::funcA()",理想应该是BaseB::funcB(),由于pbb在pa
BaseB* pbe = (BaseB*)pa; // oops!!
BaseB* pbc = dynamic_cast<BaseB*>(pa);
cout << "pa = " << pa << endl;
cout << "pb = " << pb << endl;
cout << "pbe = " << pbe << endl;
cout << "pbc = " << pbc << endl;
7.3)重点:工程开发中的多重继承方式:单继承某个类+实现(多个接口),巧妙 多看
特点:
解决了多余冗杂成员的问题
1)父类提供成员函数用于判断指针是否指向当前对象
7.3.1)使用多重继承的工程建议:
1)但用单继承多接口的方式(继承一个父类,然后实现多个接口)
2)父类中提供equal函数,判断当前指针地址是否指向该对象
3)与多重继承相关的强制转换用dynamic_cast完成
bool equal(Base* obj) //判断参数指针是不是指向当前对象
{
return (this == obj);
}
7.3.2)单继承多接口实例
class Base
{
protected:
int mi;
public:
Base(int i)
{
mi = i;
}
int getI()
{
return mi;
}
bool equal(Base* obj) //判断参数指针是不是指向当前对象
{
return (this == obj);
}
};
class Interface1
{
public:
virtual void add(int i) = 0;
virtual void minus(int i) = 0;
};
class Interface2
{
public:
virtual void multiply(int i) = 0;
virtual void divide(int i) = 0;
};
class Derived : public Base, public Interface1, public Interface2
{
public:
Derived(int i) : Base(i)
{
}
void add(int i)
{
mi += i;
}
void minus(int i)
{
mi -= i;
}
void multiply(int i)
{
mi *= i;
}
void divide(int i)
{
if( i != 0 )
{
mi /= i;
}
}
};
int main()
{
Derived d(100);
Derived* p = &d;
Interface1* pInt1 = &d;
Interface2* pInt2 = &d;
cout << "p->getI() = " << p->getI() << endl; // 100
pInt1->add(10);
pInt2->divide(11);
pInt1->minus(5);
pInt2->multiply(8);
cout << "p->getI() = " << p->getI() << endl; // 40
cout << endl;
//错误示范:这样会编译出错
//本意: p->equal((pInt1),判断pInt1是不是指向当前对象p
//错误原因: bool equal(Base* obj),参数是一个指向Base的对象,所以直接调用会出错,所以需强制类型转换,
//引入使用dynamic_cast<Base*>(pInt1)强制转换,解决了p1==p p2==p, 多重继承来的对象可能拥有“不同的地址的问题
cout << "pInt1 == p : " << p->equal((pInt1)) << endl;
cout << "pInt2 == p : " << p->equal(pInt2) << endl;
cout << "pInt1 == p : " << p->equal(dynamic_cast<Base*>(pInt1)) << endl; //dynamic_Cast用于应对多重继承,地址可能不同的问题
cout << "pInt2 == p : " << p->equal(dynamic_cast<Base*>(pInt2)) << endl;
return 0;
}
8)C++ 对象模型分析
**1)C++中的类成员对象在内存分布时和结构体相同==》程序运行时对象退化为结构体
2)类的成员变量(在内存区依次排放)和成员函数(编译后存放在代码段)分开存放,子类对象的成员变量是父类对象的成员变量+子类对象
成员变量间可能存在内存空隙 ->跟结构体一样,遵循字节对齐
3)可以通过内存地址直接访问成员变量
4)访问权限关键字在运行时失效?,在编码时有效,程序执行时无效,可以被改变?(private的成员不能在类的外部被访问,被打破了)
答:编译后变成二进制可执行文件后,就没有了访问权限的限制,依旧可以用指针/访问内存的方式修改内存的值)
5)成员函数跟成员对象存放在两个区域,成员函数怎么访问成员对象?
答:通常是用 “.” 操作符访问成员函数=>成员函数通过对象地址访问成员变量
问:对象地址怎么来的?
答: c++语法规则隐藏了对象地址的传递过程==>编译器隐式传递(this指针)->隐藏的this指针包含了对象的地址>成员函数调用this指针访问成员对象**
#include
#include
using namespace std;
class A
{ //默认是private等级
int i;
int j;
char c;
double d;
public:
void print()
{
cout << "i = " << i << ", "
<< "j = " << j << ", "
<< "c = " << c << ", "
<< "d = " << d << endl;
}
};
struct B //对象的内存分布跟结构体一样
{
int i;
int j;
char c;
double d;
};
int main()
{
A a;
cout << "sizeof(A) = " << sizeof(A) << endl; // 20 bytes
cout << "sizeof(a) = " << sizeof(a) << endl;
cout << "sizeof(B) = " << sizeof(B) << endl; // 20 bytes
a.print();
B* p = reinterpret_cast<B*>(&a); //reinterpret_cast解引用后 用结构体类型的指针p 指向对象的地址
p->i = 1; //可以通过内存地址(指针)直接访问成员变量
p->j = 2;
p->c = 'c';
p->d = 3;
a.print();
p->i = 100; //访问权限关键字在运行时失效,没有访问权限的限制=>依旧可以用指针/访问内存的方式修改内存的值)
p->j = 200;
p->c = 'C';
p->d = 3.14;
a.print();
return 0;
}