121、自动类型转换
对于内置类型,如果两种数据类型是兼容的,C++可以自动转换,如果从更大的数转换为更小的数,
可能会被截断或损失精度。
long count = 8; // int 转换为 long
double time = 11; // int 转换为 double
int side = 3.33 // double 转换为 int 的 3
C++不自动转换不兼容的类型,下面语句是非法的:
int* ptr = 8;
不能自动转换时,可以使用强制类型转换:
int* p = (int*)8;
如果某种类型与类相关,从某种类型转换为类类型是有意义的。
string str = "我是一只傻傻鸟。";
在 C++中,将一个参数的构造函数用作自动类型转换函数,它是自动进行的,不需要显式的转换。
CGirl g1(8); // 常规的写法。
CGirl g1 = CGirl(8); // 显式转换。
CGirl g1 = 8; // 隐式转换。
CGirl g1; // 创建对象。
g1 = 8; // 隐式转换,用 CGirl(8)创建临时对象,再赋值给 g。
注意:
1)一个类可以有多个转换函数。
2)多个参数的构造函数,除第一个参数外,如果其它参数都有缺省值,也可以作为转换函数。
3)CGirl(int)的隐式转换的场景:
将 CGirl 对象初始化为 int 值时。 CGirl g1 = 8;
将 int 值赋给 CGirl 对象时。 CGirl g1; g1 = 8;
将 int 值传递给接受 CGirl 参数的函数时。
返回值被声明为 CGirl 的函数试图返回 int 值时。
在上述任意一种情况下,使用可转换为 int 类型的内置类型时。
4)如果自动类型转换有二义性,编译将报错。
将构造函数用作自动类型转换函数似乎是一项不错的特性,但有时候会导致意外的类型转换。explic
it 关键字用于关闭这种自动特性,但仍允许显式转换。
explicit CGirl(int bh);
CGirl g=8; // 错误。
CGirl g=CGirl(8); // 显式转换,可以。
CGirl g=(CGirl)8; // 显式转换,可以。
在实际开发中,如果强调的是构造,建议使用 explicit,如果强调的是类型转换,则不使用 explicit。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
int m_bh; // 编号。
string m_name; // 姓名。
double m_weight; // 体重,单位:kg。
// 默认构造函数。
CGirl() { m_bh = 0; m_name.clear(); m_weight = 0; cout << "调用了 CGirl()\n"; }
// 自我介绍的方法。
void show() { cout << "bh=" << m_bh << ",name=" << m_name << ",weight=" <<
m_weight << endl; }
explicit CGirl(int bh) { m_bh = bh; m_name.clear(); m_weight = 0; cout << "调用了
CGirl(int bh)\n"; }
//CGirl(double weight) { m_bh = 0; m_name.clear(); m_weight = weight; cout << "调
用了 CGirl(double weight)\n"; }
};
int main()
{
//CGirl g1(8); // 常规的写法。
//CGirl g1 = CGirl(8); // 显式转换。
//CGirl g1 = 8; // 隐式转换。
CGirl g1; // 创建对象。
g1 = (CGirl)8; // 隐式转换,用 CGirl(8)创建临时对象,再赋值给 g。
//CGirl g1 = 8.7; // 隐式转换。
//g1.show();
}
122、转换函数
构造函数只用于从某种类型到类类型的转换,如果要进行相反的转换,可以使用特殊的运算符函数- 转换函数。
语法:operator 数据类型();
注意:转换函数必须是类的成员函数;不能指定返回值类型;不能有参数。
可以让编译器决定选择转换函数(隐式转换),可以像使用强制类型转换那样使用它们(显式转换)。
int ii=girl; // 隐式转换。
int ii=(int) girl; // 显式转换。
int ii=int(girl); // 显式转换。
如果隐式转换存在二义性,编译器将报错。
在 C++98 中,关键字 explicit 不能用于转换函数,但 C++11 消除了这种限制,可以将转换函数声
明为显式的。
还有一种方法是:用一个功能相同的普通成员函数代替转换函数,普通成员函数只有被调用时才会执
行。
int ii=girl.to_int();
警告:应谨慎的使用隐式转换函数。通常,最好选择仅在被显式地调用时才会执行的成员函数。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类 CGirl。
{
public:
int m_bh; // 编号。
string m_name; // 姓名。
double m_weight; // 体重,单位:kg。
// 默认构造函数。
CGirl() { m_bh = 8; m_name="西施"; m_weight = 50.7; }
explicit operator int() { return m_bh; }
int to_int() { return m_bh; }
operator string() { return m_name; }
explicit operator double() { return m_weight; }
};
int main()
{
string name = "西施"; // char * 转换成 string
const char* ptr = name; // string 转换成 char *,错误
const char* ptr = name.c_str(); // 返回 char *,正确
CGirl g;
int a = g.to_int(); cout << "a 的值是:" << a << endl;
string b = string(g); cout << "b 的值是:" << b << endl;
double c = double(g); cout << "c 的值是:" << c << endl;
short d = (int)g;
}
123、继承的基本概念
继承可以理解为一个类从另一个类获取成员变量和成员函数的过程。
语法:
class 派生类名:[继承方式]基类名
{
派生类新增加的成员
};
被继承的类称为基类或父类,继承的类称为派生类或子类。
继承和派生是一个概念,只是站的角度不同。
派生类除了拥有基类的成员,还可以定义新的成员,以增强其功能。
使用继承的场景:
-
如果新创建的类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承。
-
当需要创建多个类时,如果它们拥有很多相似的成员变量或成员函数,可以将这些类共同的成员
提取出来,定义为基类,然后从基类继承。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CAllComers // 海选报名者类
{
public:
string m_name; // 姓名
string m_tel; // 联系电话
// 构造函数。
CAllComers() { m_name = "某女"; m_tel = "不详"; }
// 报名时需要唱一首歌。
void sing() { cout << "我是一只小小鸟。\n"; }
// 设置姓名。
void setname(const string& name) { m_name = name; }
// 设置电话号码。
void settel(const string& tel) { m_tel = tel; }
};
class CGirl :public CAllComers // 超女类
{
public:
int m_bh; // 编号。
CGirl() { m_bh = 8; }
void show() { cout << "编号:" << m_bh << ",姓名:" << m_name << ",联系电话:" <<
m_tel << endl; }
};
int main()
{
CGirl g;
g.setname("西施");
g.show();
}
124、继承方式
类成员的访问权限由高到低依次为:public --> protected --> private,public 成员在类外可以
访问,private 成员只能在类的成员函数中访问。
如果不考虑继承关系,protected 成员和 private 成员一样,类外不能访问。但是,当存在继承关系
时,protected 和 private 就不一样了。基类中的 protected 成员可以在派生类中访问,而基类中的 pri
vate 成员不能在派生类中访问。
继承方式有三种:public(公有的)、protected(受保护的)和 private(私有的)。它是可选的,
如果不写,那么默认为 private。不同的继承方式决定了在派生类中成员函数中访问基类成员的权限。
1)基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为 protect
ed 时,那么基类成员在派生类中的访问权限最高也为 protected,高于 protected 的会降级为 protect
ed,但低于 protected 不会升级。再如,当继承方式为 public 时,那么基类成员在派生类中的访问权限
将保持不变。
也就是说,继承方式中的 public、protected、private 是用来指明基类成员在派生类中的最高访问
权限的。
- 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数
中访问或调用)。
- 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public
或 protected;只有那些不希望在派生类中使用的成员才声明为 private。
- 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明
为 protected。
由于 private 和 protected 继承方式会改变基类成员在派生类中的访问权限,导致继承关系复杂,所
以,在实际开发中,一般使用 public。
在派生类中,可以通过基类的公有成员函数间接访问基类的私有成员。
使用 using 关键字可以改变基类成员在派生类中的访问权限。
注意:using 只能改变基类中 public 和 protected 成员的访问权限,不能改变 private 成员的访问
权限,因为基类中的 private 成员在派生类中是不可见的,根本不能使用。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A { // 基类
public:
int m_a=10;
protected:
int m_b=20;
private:
int m_c = 30;
};
class B :public A // 派生类
{
public:
using A::m_b; // 把 m_b 的权限修改为公有的。
private:
using A::m_a; // 把 m_a 的权限修改为私有的。
};
int main()
{
B b;
// b.m_a = 11;
b.m_b = 21;
//b.m_c = 21;
}
125、继承的对象模型
1)创建派生类对象时,先调用基类的构造函数,再调用派生类的构造函数。
2)销毁派生类对象时,先调用派生类的析构函数,再调用基类的析构函数。如果手工调用派生类的
析构函数,也会调用基类的析构函数。
3)创建派生类对象时只会申请一次内存,派生类对象包含了基类对象的内存空间,this 指针相同的。
4)创建派生类对象时,先初始化基类对象,再初始化派生类对象。
5)在 VS 中,用 cl.exe 可以查看类的内存模型。
6)对派生类对象用 sizeof 得到的是基类所有成员(包括私有成员)+派生类对象所有成员的大小。
7)在 C++中,不同继承方式的访问权限只是语法上的处理。
8)对派生类对象用 memset()会清空基类私有成员。
9)用指针可以访问到基类中的私有成员(内存对齐)。
查看对象内存布局的方法:
cl 源文件名 /d1 reportSingleClassLayout 类名
注意:类名不要太短,否则屏幕会显示一大堆东西,找起来很麻烦。
例如,查看 BBB 类,源代码文件是 demo01.cpp:
cl demo01.cpp /d1 reportSingleClassLayoutBBB
cl 命令环境变量:
1)在 PATH 环境变量中增加 cl.exe 的目录
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.30.30705\bin\Hostx64\x64
2)增加 INCLUDE 环境变量,内容如下:
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.30.30705\include
C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\shared
C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\ucrt
C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\um
C:\Program Files (x86)\Windows Kits\10\Include\10.0.19041.0\winrt
3)增加 LIB 环境变量,内容如下:
C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Tools\MSVC\14.30.30705\lib\x64
C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\um\x64
C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\ucrt\x64
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void* operator new(size_t size) // 重载 new 运算符。
{
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << ",大小是:" << size << endl;
return ptr;
}
void operator delete(void* ptr) // 重载 delete 运算符。
{
if (ptr == 0) return; // 对空指针 delete 是安全的。
free(ptr); // 释放内存。
cout << "释放了内存。\n";
}
class A { // 基类
public:
int m_a = 10;
protected:
int m_b = 20;
private:
int m_c = 30;
public:
A() {
cout << "A 中 this 指针是: " << this << endl;
cout << "A 中 m_a 的地址是:" << &m_a << endl;
cout << "A 中 m_b 的地址是:" << &m_b << endl;
cout << "A 中 m_c 的地址是:" << &m_c << endl;
}
void func() { cout << "m_a=" << m_a << ",m_b=" << m_b << ",m_c=" << m_c <<
endl; }
};
class B :public A // 派生类
{
public:
int m_d = 40;
B() {
cout << "B 中 this 指针是: " << this << endl;
cout << "B 中 m_a 的地址是:" << &m_a << endl;
cout << "B 中 m_b 的地址是:" << &m_b << endl;
//cout << "B 中 m_c 的地址是:" << &m_c << endl;
cout << "B 中 m_d 的地址是:" << &m_d << endl;
}
void func1() { cout << "m_d=" << m_d << endl; }
};
int main()
{
cout << "基类占用内存的大小是:" << sizeof(A) << endl;
cout << "派生类占用内存的大小是:" << sizeof(B) << endl;
B *p=new B;
p->func(); p->func1();
// memset(p, 0, sizeof(B)); *((int*)p + 2) = 31; // 把基类私有成员 m_c 的值修改成 31。
p->func(); p->func1();
delete p;
}
126、如何构造基类
派生类构造函数的要点如下:
1)创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
2)如果没以指定基类构造函数,将使用基类的默认构造函数。
3)可以用初始化列表指明要使用的基类构造函数。
4)基类构造函数负责初始化被继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。
5)派生类的构造函数总是调用一个基类构造函数,包括拷贝构造函数。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A { // 基类
public:
int m_a;
private:
int m_b;
public:
A() : m_a(0) , m_b(0) // 基类的默认构造函数。
{
cout << "调用了基类的默认构造函数 A()。\n";
}
A(int a,int b) : m_a(a) , m_b(b) // 基类有两个参数的构造函数。
{
cout << "调用了基类的构造函数 A(int a,int b)。\n";
}
A(const A &a) : m_a(a.m_a+1) , m_b(a.m_b+1) // 基类的拷贝构造函数。
{
cout << "调用了基类的拷贝构造函数 A(const A &a)。\n";
}
// 显示基类 A 全部的成员。
void showA() { cout << "m_a=" << m_a << ",m_b=" << m_b << endl; }
};
class B :public A // 派生类
{
public:
int m_c;
B() : m_c(0) , A() // 派生类的默认构造函数,指明用基类的默认构造函数(不指
明也无所谓)。
{
cout << "调用了派生类的默认构造函数 B()。\n";
}
B(int a, int b, int c) : A(a, b), m_c(c) // 指明用基类的有两个参数的构造函数。
{
cout << "调用了派生类的构造函数 B(int a,int b,int c)。\n";
}
B(const A& a, int c) :A(a), m_c(c) // 指明用基类的拷贝构造函数。
{
cout << "调用了派生类的构造函数 B(const A &a,int c) 。\n";
}
// 显示派生类 B 全部的成员。
void showB() { cout << "m_c=" << m_c << endl << endl; }
};
int main()
{
B b1; // 将调用基类默认的构造函数。
b1.showA(); b1.showB();
B b2(1, 2, 3); // 将调用基类有两个参数的构造函数。
b2.showA(); b2.showB();
A a(10, 20); // 创建基类对象。
B b3(a, 30); // 将调用基类的拷贝造函数。
b3.showA(); b3.showB();
}
127、名字遮蔽与类作用域
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,通过派生类对象或者在派生
类的成员函数中使用该成员时,将使用派生类新增的成员,而不是基类的。
注意:基类的成员函数和派生类的成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基
类中的所有同名函数。
类是一种作用域,每个类都有它自己的作用域,在这个作用域之内定义成员。
在类的作用域之外,普通的成员只能通过对象(可以是对象本身,也可以是对象指针或对象引用)来
访问,静态成员可以通过对象访问,也可以通过类访问。
在成员名前面加类名和域解析符可以访问对象的成员。
如果不存在继承关系,类名和域解析符可以省略不写。
当存在继承关系时,基类的作用域嵌套在派生类的作用域中。如果成员在派生类的作用域中已经找到,
就不会在基类作用域中继续查找;如果没有找到,则继续在基类作用域中查找。
如果在成员的前面加上类名和域解析符,就可以直接使用该作用域的成员。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A { // 基类
public:
int m_a=10;
void func() { cout << "调用了 A 的 func()函数。\n"; }
};
class B :public A { // 子类
public:
int m_a = 20;
void func() { cout << "调用了 B 的 func()函数。\n"; }
};
class C :public B { // 孙类
public:
int m_a = 30;
void func() { cout << "调用了 C 的 func()函数。\n"; }
void show() {
cout << "C::m_a 的值是:" << C::m_a << endl;
cout << "B::m_a 的值是:" << B::m_a << endl;
cout << "A::m_a 的值是:" << B::A::m_a << endl;
}
};
int main()
{
C c;
cout << "C::m_a 的值是:" << c.C::m_a << endl;
cout << "B::m_a 的值是:" << c.B::m_a << endl;
cout << "A::m_a 的值是:" << c.B::A::m_a << endl;
c.C::func();
c.B::func();
c.B::A::func();
}
128、继承的特殊关系
派生类和基类之间有一些特殊关系。
1)如果继承方式是公有的,派生类对象可以使用基类成员。
2)可以把派生类对象赋值给基类对象(包括私有成员),但是,会舍弃非基类的成员。
3)基类指针可以在不进行显式转换的情况下指向派生类对象。
4)基类引用可以在不进行显式转换的情况下引用派生类对象。
注意:
1)基类指针或引用只能调用基类的方法,不能调用派生类的方法。
2)可以用派生类构造基类。
3)如果函数的形参是基类,实参可以用派生类。
4)C++要求指针和引用类型与赋给的类型匹配,这一规则对继承来说是例外。但是,这种例外只是
单向的,不可以将基类对象和地址赋给派生类引用和指针(没有价值,没有讨论的必要)。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A { // 基类
public:
int m_a=0;
private:
int m_b=0;
public:
// 显示基类 A 全部的成员。
void show() { cout << "A::show() m_a=" << m_a << ",m_b=" << m_b << endl; }
// 设置成员 m_b 的值。
void setb(int b) { m_b = b; }
};
class B :public A // 派生类
{
public:
int m_c=0;
// 显示派生类 B 全部的成员。
void show() { cout << "B::show() m_a=" << m_a << "m_c=" << m_c << endl; }
};
int main()
{
B b;
A* a = &b;
b.m_a = 10;
b.setb(20); // 设置成员 m_b 的值。
b.m_c = 30;
b.show(); // 调用的是 B 类的 show()函数。
a->m_a = 11;
a->setb(22); // 设置成员 m_b 的值。
// a->m_c = 30;
a->show(); // 调用的是 A 类的 show()函数。
}
129、多继承与虚继承
多继承的语法:
class 派生类名 : [继承方式 1] 基类名 1, [继承方式 2] 基类名 2,...... {
派生类新增加的成员
};
菱形继承
虚继承可以解决菱形继承的二义性和数据冗余的问题。
有了多继承,就存在菱形继承,有了菱形继承就有虚继承,增加了复杂性。
不提倡使用多继承,只有在比较简单和不出现二义性的情况时才使用多继承,能用单一继承解决的问
题就不要使用多继承。
如果继承的层次很多、关系很复杂,程序的编写、调试和维护工作都会变得更加困难,由于这个原因,
C++之后的很多面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。
多继承示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A1 { // 基类一
public:
int m_a = 10;
};
class A2 { // 基类二
public:
int m_a = 20;
};
class B :public A1, public A2 { // 派生类
public:
int m_a = 30;
};
int main()
{
B b;
cout << " B::m_a 的值是:" << b.m_a << endl;
cout << "A1::m_a 的值是:" << b.A1::m_a << endl;
cout << "A2::m_a 的值是:" << b.A2::m_a << endl;
}
菱形继承示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class A {
public:
int m_a = 10;
};
class B : virtual public A { };
class C : virtual public A { };
class DD : public B, public C {};
int main()
{
DD d;
// d.B::m_a = 30;
// d.C::m_a = 80;
d.m_a = 80;
cout << "B::m_a 的地址是:" << &d.B::m_a << ",值是:" << d.B::m_a << endl;
cout << "C::m_a 的地址是:" << &d.C::m_a << ",值是:" << d.C::m_a << endl;
}
131、多态的基本概念
基类指针只能调用基类的成员函数,不能调用派生类的成员函数。
如果在基类的成员函数前加 virtual 关键字,把它声明为虚函数,基类指针就可以调用派生类中同名
的成员函数,通过派生类中同名的成员函数,就可以访问派生对象的成员变量。
有了虚函数,基类指针指向基类对象时就使用基类的成员函数和数据,指向派生类对象时就使用派生
类的成员函数和数据,基类指针表现出了多种形式,这种现象称为多态。
基类引用也可以使用多态。
注意:
1)只需要在基类的函数声明中加上 virtual 关键字,函数定义时不能加。
2)在派生类中重定义虚函数时,函数特征要相同。
3)当在基类中定义了虚函数时,如果派生类没有重定义该函数,那么将使用基类的虚函数。
4)在派生类中重定义了虚函数的情况下,如果想使用基类的虚函数,可以加类名和域解析符。
5)如果要在派生类中重新定义基类的函数,则将它设置为虚函数;否则,不要设置为虚函数,有两
方面的好处:首先效率更高;其次,指出不要重新定义该函数。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CAllComers { // 报名者类
public:
int m_bh = 0; // 编号。
virtual void show() { cout << "CAllComers::show():我是" << m_bh << "号。 " << endl; }
virtual void show(int a) { cout << "CAllComers::show(int a):我是" << m_bh << "号。" <<
endl; }
};
class CGirl :public CAllComers { // 超女类
public:
int m_age = 0; // 年龄。
void show() { cout << "CGirl::show():我是" << m_bh << "号, " << m_age << "岁。" <<
endl; }
void show(int a) { cout << "CGirl::show(int a):我是" << m_bh << "号, " << m_age <<
"岁。" << endl; }
};
int main()
{
CAllComers a; a.m_bh = 3; // 创建基类对象并对成员赋值。
CGirl g; g.m_bh = 8; g.m_age = 23; // 创建派生类对象并对成员赋值。
CAllComers* p; // 声明基类指针。
//p = &a; p->show(); // 让基类指针指向基类对象,并调用虚函数。
p = &g; p->show(); // 让基类指针指向派生类对象,并调用虚函数。
p->show(5);
p->CAllComers::show(5);
}
132、多态的应用场景
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class Hero // 英雄基类
{
public:
int viability; // 生存能力。
int attack; // 攻击伤害。
virtual void skill1() { cout << "英雄释放了一技能。\n"; }
virtual void skill2() { cout << "英雄释放了二技能。\n"; }
virtual void uskill() { cout << "英雄释放了大绝招。\n"; }
};
class XS :public Hero // 西施派生类
{
public:
void skill1() { cout << "西施释放了一技能。\n"; }
void skill2() { cout << "西施释放了二技能。\n"; }
void uskill() { cout << "西施释放了大招。\n"; }
};
class HX :public Hero // 韩信派生类
{
public:
void skill1() { cout << "韩信释放了一技能。\n"; }
void skill2() { cout << "韩信释放了二技能。\n"; }
void uskill() { cout << "韩信释放了大招。\n"; }
};
class LB :public Hero // 李白派生类
{
public:
void skill1() { cout << "李白释放了一技能。\n"; }
void skill2() { cout << "李白释放了二技能。\n"; }
void uskill() { cout << "李白释放了大招。\n"; }
};
int main()
{
// 根据用户选择的英雄,施展一技能、二技能和大招。
int id = 0; // 英雄的 id。
cout << "请输入英雄(1-西施;2-韩信;3-李白。):";
cin >> id;
// 创建基类指针,让它指向派生类对象,用基类指针调用派生类的成员函数。
Hero* ptr = nullptr;
if (id == 1) { // 1-西施
ptr=new XS;
}
else if (id == 2) { // 2-韩信
ptr = new HX;
}
else if (id == 3) { // 3-李白
ptr = new LB;
}
if (ptr != nullptr) {
ptr->skill1();
ptr->skill2();
ptr->uskill();
delete ptr;
}
}
133、多态的对象模型
类的普通成员函数的地址是静态的,在编译阶段已指定。
如果基类中有虚函数,对象的内存模型中有一个虚函数表,表中存放了基类的函数名和地址。
如果派生类中重定义了基类的虚函数,创建派生类对象时,将用派生类的函数取代虚函数表中基类的
函数。
C++中的多态分为两种:静态多态与动态多态。
静态多态:也成为编译时的多态;在编译时期就已经确定要执行了的函数地址了;主要有函数重载和
函数模板。
动态多态:即动态绑定,在运行时才去确定对象类型和正确选择需要调用的函数,一般用于解决基类
指针或引用派生类对象调用类中重写的方法(函数)时出现的问题。
134、如何析构派生类
构造函数不能继承,创建派生类对象时,先执行基类构造函数,再执行派生类构造函数。
析构函数不能继承,而销毁派生类对象时,先执行派生类析构函数,再执行基类析构函数。
派生类的析构函数在执行完后,会自动执行基类的析构函数。
如果手工的调用派生类的析构函数,也会自动调用基类的析构函数。
析构派生类的要点如下:
1)析构派生类对象时,会自动调用基类的析构函数。与构造函数不同的是,在派生类的析构函数中
不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。
2)析构函数可以手工调用,如果对象中有堆内存,析构函数中以下代码是必要的:
delete ptr;
ptr=nulllptr;
3)用基类指针指向派生类对象时,delete 基类指针调用的是基类的析构函数,不是派生类的,如果
希望调用派生类的析构函数,就要把基类的析构函数设置为虚函数。
4)C++编译器对虚析构函数做了特别的处理。
5)对于基类,即使它不需要析构函数,也应该提供一个空虚析构函数。
6)赋值运算符函数不能继承,派生类继承的函数的特征标与基类完全相同,但赋值运算符函数的特
征标随类而异,它包含了一个类型为其所属类的形参。
7)友元函数不是类成员,不能继承。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class AA { // 基类
public:
AA() { cout << "调用了基类的构造函数 AA()。\n"; }
virtual void func() { cout << "调用了基类的 func()。\n"; }
// virtual ~AA() { cout << "调用了基类的析构函数~AA()。\n"; }
virtual ~AA() {}
};
class BB:public AA { // 派生类
public:
BB() { cout << "调用了派生类的构造函数 BB()。\n"; }
void func() { cout << "调用了派生类的 func()。\n"; } ~BB() { cout << "调用了派生类的析构函数~BB()。\n"; }
};
int main()
{
AA *a=new BB;
delete a;
}
135、纯虚函数和抽象类
纯虚函数是一种特殊的虚函数,在某些情况下,基类中不能对虚函数给出有意义的实现,把它声明为
纯虚函数。
纯虚函数只有函数名、参数和返回值类型,没有函数体,具体实现留给该派生类去做。
语法:virtual 返回值类型 函数名 (参数列表)=0;
纯虚函数在基类中为派生类保留一个函数的名字,以便派生类它进行重定义。如果在基类中没有保留
函数名字,则无法支持多态性。
含有纯虚函数的类被称为抽象类,不能实例化对象,可以创建指针和引用。
派生类必须重定义抽象类中的纯虚函数,否则也属于抽象类。
基类中的纯虚析构函数也需要实现。
有时候,想使一个类成为抽象类,但刚好又没有任何纯虚函数,怎么办?
方法很简单:在想要成为抽象类的类中声明一个纯虚析构函数。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class AA { // 基类
public:
AA() { cout << "调用了基类的构造函数 AA()。\n"; }
virtual void func() = 0 { cout << "调用了基类的 func()。\n"; }
virtual ~AA() = 0 { cout << "调用了基类的析构函数~AA()。\n"; }
};
class BB :public AA { // 派生类
public:
BB() { cout << "调用了派生类的构造函数 BB()。\n"; }
void func() { cout << "调用了派生类的 func()。\n"; } ~BB() { cout << "调用了派生类的析构函数~BB()。\n"; }
};
int main()
{
BB b;
AA &r = b;
r.func();
}
136、运行阶段类型识别 dynamic_cast
运行阶段类型识别(RTTI RunTime Type Identification)为程序在运行阶段确定对象的类型,只
适用于包含虚函数的类。
基类指针可以指向派生类对象,如何知道基类指针指向的是哪种派生类的对象呢?(想调用派生类中
的非虚函数)。
dynamic_cast 运算符用指向基类的指针来生成派生类的指针,它不能回答"指针指向的是什么类的
对象"的问题,但能回答"是否可以安全的将对象的地址赋给特定类的指针"的问题。
语法:派生类指针 = dynamic_cast<派生类类型 *>(基类指针);
如果转换成功,dynamic_cast 返回对象的地址,如果失败,返回 nullptr。
注意:
1)dynamic_cast 只适用于包含虚函数的类。
2)dynamic_cast 可以将派生类指针转换为基类指针,这种画蛇添足的做法没有意义。
3)dynamic_cast 可以用于引用,但是,没有与空指针对应的引用值,如果转换请求不正确,会出
现 bad_cast 异常。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class Hero // 英雄基类
{
public:
int viability; // 生存能力。
int attack; // 攻击伤害。
virtual void skill1() { cout << "英雄释放了一技能。\n"; }
virtual void skill2() { cout << "英雄释放了二技能。\n"; }
virtual void uskill() { cout << "英雄释放了大绝招。\n"; }
};
class XS :public Hero // 西施派生类
{
public:
void skill1() { cout << "西施释放了一技能。\n"; }
void skill2() { cout << "西施释放了二技能。\n"; }
void uskill() { cout << "西施释放了大招。\n"; }
void show() { cout << "我是天下第一美女。\n"; }
};
class HX :public Hero // 韩信派生类
{
public:
void skill1() { cout << "韩信释放了一技能。\n"; }
void skill2() { cout << "韩信释放了二技能。\n"; }
void uskill() { cout << "韩信释放了大招。\n"; }
};
class LB :public Hero // 李白派生类
{
public:
void skill1() { cout << "李白释放了一技能。\n"; }
void skill2() { cout << "李白释放了二技能。\n"; }
void uskill() { cout << "李白释放了大招。\n"; }
};
int main()
{
// 根据用户选择的英雄,施展一技能、二技能和大招。
int id = 0; // 英雄的 id。
cout << "请输入英雄(1-西施;2-韩信;3-李白。):";
cin >> id;
// 创建基类指针,让它指向派生类对象,用基类指针调用派生类的成员函数。
Hero* ptr = nullptr;
if (id == 1) { // 1-西施
ptr = new XS;
}
else if (id == 2) { // 2-韩信
ptr = new HX;
}
else if (id == 3) { // 3-李白
ptr = new LB;
}
if (ptr != nullptr) {
ptr->skill1();
ptr->skill2();
ptr->uskill();
// 如果基类指针指向的对象是西施,那么就调用西施的 show()函数。
//if (id == 1) {
// XS* pxs = (XS *)ptr; // C 风格强制转换的方法,程序员必须保证目标类型正确。
// pxs->show();
//}
XS* xsptr = dynamic_cast<XS*>(ptr); // 把基类指针转换为派生类。
if (xsptr != nullptr) xsptr->show(); // 如果转换成功,调用派生类西施的非
虚函数。
delete ptr;
}
// 以下代码演示把基类引用转换为派生类引用时发生异常的情况。
/*HX hx;
Hero& rh = hx;
try{
XS & rxs= dynamic_cast<XS &>(rh);
}
catch (bad_cast) {
cout << "出现了 bad_cast 异常。\n";
}*/
}
137、typeid 运算符和 type_info 类
typeid 运算符用于获取数据类型的信息。
语法一:typeid(数据类型);
语法二:typeid(变量名或表达式);
typeid 运算符返回 type_info 类(在头文件<typeinfo>中定义)的对象的引用。
type_info 类的实现随编译器而异,但至少有 name()成员函数,该函数返回一个字符串,通常是类
名。
type_info 重载了==和!=运算符,用于对类型进行比较。
注意:
1)type_info 类的构造函数是 private 属性,也没有拷贝构造函数,所以不能直接实例化,只能由编
译器在内部实例化。
2)不建议用 name()成员函数返回的字符串作为判断数据类型的依据。(编译器可能会转换类型名)
3)typeid 运算符可以用于多态的场景,在运行阶段识别对象的数据类型。
4)假设有表达式 typeid(*ptr),当 ptr 是空指针时,如果 ptr 是多态的类型,将引发 bad_typeid 异
常。
示例:
#include <iostream>
#include <string>
using namespace std;
class AA { // 定义一个类。
public:
AA() {}
};
int main()
{
// typeid 用于自定义的数据类型。
AA aa;
AA* paa = &aa;
AA& raa = aa;
cout << "typeid(AA)=" << typeid(AA).name() << endl;
cout << "typeid(aa)=" << typeid(aa).name() << endl;
cout << "typeid(AA *)=" << typeid(AA*).name() << endl;
cout << "typeid(paa)=" << typeid(paa).name() << endl;
cout << "typeid(AA &)=" << typeid(AA&).name() << endl;
cout << "typeid(raa)=" << typeid(raa).name() << endl;
// type_info 重载了==和!=运算符,用于对类型进行比较。
if (typeid(AA) == typeid(aa)) cout << "ok1\n";
if (typeid(AA) == typeid(*paa)) cout << "ok2\n";
if (typeid(AA) == typeid(raa)) cout << "ok3\n";
if (typeid(AA*) == typeid(paa)) cout << "ok4\n";
return 0;
}
140、自动推导类型 auto
在 C 语言和 C++98 中,auto 关键字用于修饰变量(自动存储的局部变量)。
在 C++11 中,赋予了 auto 全新的含义,不再用于修饰变量,而是作为一个类型指示符,指示编译
器在编译时推导 auto 声明的变量的数据类型。
语法:auto 变量名 = 初始值;
在 Linux 平台下,编译需要加-std=c++11 参数。
注意:
1)auto 声明的变量必须在定义时初始化。
2)初始化的右值可以是具体的数值,也可以是表达式和函数的返回值等。
3)auto 不能作为函数的形参类型。
4)auto 不能直接声明数组。
5)auto 不能定义类的非静态成员变量。
不要滥用 auto,auto 在编程时真正的用途如下:
1)代替冗长复杂的变量声明。
2)在模板中,用于声明依赖模板参数的变量。
3)函数模板依赖模板参数的返回值。
4)用于 lambda 表达式中。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
double func(double b, const char* c, float d, short e, long f)
{
cout << ",b=" << b << ",c=" << c << ",d=" << d << ",e=" << e << ",f=" << f << endl;
return 5.5;
}
int main()
{
double (*pf)( double , const char* , float , short , long ); // 声明函数指针 pf。
pf = func;
pf( 2, "西施", 3, 4, 5);
auto pf1 = func;
pf1(2, "西施", 3, 4, 5);
}
141、函数模板的基本概念
函数模板是通用的函数描述,使用任意类型(泛型)来描述函数。
编译的时候,编译器推导实参的数据类型,根据实参的数据类型和函数模板,生成该类型的函数定义。
生成函数定义的过程被称为实例化。
创建交换两个变量的函数模板:
template <typename T>
void Swap(T &a, T &b)
{
T tmp = a;
a = b;
b = tmp;
}
在 C++98 添加关键字 typename 之前,C++使用关键字 class 来创建模板。
如果考虑向后兼容,函数模板应使用 typename,而不是 class。
函数模板实例化可以让编译器自动推导,也可以在调用的代码中显式的指定。
142、函数模板的注意事项
1)可以为类的成员函数创建模板,但不能是虚函数和析构函数。
2)使用函数模板时,必须明确数据类型,确保实参与函数模板能匹配上。
3)使用函数模板时,推导的数据类型必须适应函数模板中的代码。
4)使用函数模板时,如果是自动类型推导,不会发生隐式类型转换,如果显式指定了函数模板的数
据类型,可以发生隐式类型转换。
5)函数模板支持多个通用数据类型的参数。
6)函数模板支持重载,可以有非通用数据类型的参数。
144、函数模板的具体化
可以提供一个具体化的函数定义,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,不
再寻找模板。
具体化(特例化、特化)的语法:
template<> void 函数模板名<数据类型>(参数列表)
template<> void 函数模板名 (参数列表)
{
// 函数体。
}
对于给定的函数名,可以有普通函数、函数模板和具体化的函数模板,以及它们的重载版本。
编译器使用各种函数的规则:
1)具体化优先于常规模板,普通函数优先于具体化和常规模板。
2)如果希望使用函数模板,可以用空模板参数强制使用函数模板。
3)如果函数模板能产生更好的匹配,将优先于普通函数。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类。
{
public:
int m_bh; // 编号。
string m_name; // 姓名。
int m_rank; // 排名。
};
template <typename T>
void Swap(T& a, T& b); // 交换两个变量的值函数模板。
template<>
void Swap<CGirl>(CGirl& g1, CGirl& g2); // 交换两个超女对象的排名。
// template<>
// void Swap(CGirl& g1, CGirl& g2); // 交换两个超女对象的排名。
int main()
{
int a = 10, b = 20;
Swap(a, b); // 使用了函数模板。
cout << "a=" << a << ",b=" << b << endl;
CGirl g1, g2;
g1.m_rank = 1; g2.m_rank = 2;
Swap(g1, g2); // 使用了超女类的具体化函数。
cout << "g1.m_rank=" << g1.m_rank << ",g2.m_rank=" << g2.m_rank << endl;
}
template <typename T>
void Swap(T& a, T& b) // 交换两个变量的值函数模板。
{
T tmp = a;
a = b;
b = tmp;
cout << "调用了 Swap(T& a, T& b)\n";
}
template<>
void Swap<CGirl>(CGirl& g1, CGirl& g2) // 交换两个超女对象的排名。
// template<>
// void Swap(CGirl& g1, CGirl& g2) // 交换两个超女对象的排名。
{
int tmp = g1.m_rank;
g1.m_rank = g2.m_rank;
g2.m_rank = tmp;
cout << "调用了 Swap(CGirl& g1, CGirl& g2)\n";
}
//
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void Swap(int a, int b) // 普通函数。
{
cout << "使用了普通函数。\n";
}
template <typename T>
void Swap(T a, T b) // 函数模板。
{
cout << "使用了函数模板。\n";
}
template <>
void Swap(int a, int b) // 函数模板的具体化版本。
{
cout << "使用了具体化的函数模板。\n";
}
int main()
{
Swap('c', 'd');
}
145、函数模板分文件编写
函数模板只是函数的描述,没有实体,创建函数模板的代码放在头文件中。
函数模板的具体化有实体,编译的原理和普通函数一样,所以,声明放在头文件中,定义放在源文件
中。
示例:
/
// public.h
#pragma once
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void Swap(int a, int b); // 普通函数。
template <typename T>
void Swap(T a, T b) // 函数模板。
{
cout << "使用了函数模板。\n";
}
template <>
void Swap(int a, int b); // 函数模板的具体化版本。
/
/
// public.cpp
#include "public.h" void Swap(int a, int b) // 普通函数。
{
cout << "使用了普通函数。\n";
}
template <>
void Swap(int a, int b) // 函数模板的具体化版本。
{
cout << "使用了具体化的函数模板。\n";
}
/
/
// demo01.cpp
#include "public.h"
int main()
{
Swap(1,2); // 将使用普通函数。
Swap(1.3, 3.5); // 将使用具体化的函数模板。
Swap('c', 'd'); // 将使用函数模板。
}
/
146、函数模板高级
1)decltype 关键字
在 C++11 中,decltype 操作符,用于查询表达式的数据类型。
语法:decltype(expression) var;
decltype 分析表达式并得到它的类型,不会计算执行表达式。函数调用也一种表达式,因此不必担
心在使用 decltype 时执行了函数。
decltype 推导规则(按步骤):
1)如果 expression 是一个没有用括号括起来的标识符,则 var 的类型与该标识符的类型相同,包括
const 等限定符。
2)如果 expression 是一个函数调用,则 var 的类型与函数的返回值类型相同(函数不能返回 void,
但可以返回 void *)。
3)如果 expression 是一个左值(能取地址)(要排除第一种情况)、或者用括号括起来的标识符,那
么 var 的类型是 expression 的引用。
4)如果上面的条件都不满足,则 var 的类型与 expression 的类型相同。
如果需要多次使用 decltype,可以结合 typedef 和 using。
2)函数后置返回类型
int func(int x,double y);
等同:
auto func(int x,double y) -> int;
将返回类型移到了函数声明的后面。
auto 是一个占位符(C++11 给 auto 新增的角色), 为函数返回值占了一个位置。
这种语法也可以用于函数定义:
auto func(int x,double y) -> int
{
// 函数体。
}
3)C++14 的 auto 关键字
C++14 标准对函数返回类型推导规则做了优化,函数的返回值可以用 auto,不必尾随返回类型。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
template <typename T1, typename T2>
auto func(T1 x, T2 y) -> decltype(x + y)
{
// 其它的代码。
decltype(x+y) tmp = x + y;
cout << "tmp=" << tmp << endl;
return tmp;
}
int main()
{
func(3, 5.8);
}
150、模板类的基本概念
类模板是通用类的描述,使用任意类型(泛型)来描述类的定义。
使用类模板的时候,指定具体的数据类型,让编译器生成该类型的类定义。
语法:
template <class T>
class 类模板名
{
类的定义;
};
函数模板建议用 typename 描述通用数据类型,类模板建议用 class。
注意:
1)在创建对象的时候,必须指明具体的数据类型。
2)使用类模板时,数据类型必须适应类模板中的代码。
3)类模板可以为通用数据类型指定缺省的数据类型(C++11 标准的函数模板也可以)。
4)模板类的成员函数可以在类外实现。
5)可以用 new 创建模板类对象。
6)在程序中,模板类的成员函数使用了才会创建。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
template <class T1, class T2=string>
class AA
{
public:
T1 m_a; // 通用类型用于成员变量。
T2 m_b; // 通用类型用于成员变量。
AA() { } // 默认构造函数是空的。
// 通用类型用于成员函数的参数。
AA(T1 a,T2 b):m_a(a),m_b(b) { }
// 通用类型用于成员函数的返回值。
T1 geta() // 获取成员 m_a 的值。
{
T1 a = 2; // 通用类型用于成员函数的代码中。
return m_a + a;
}
T2 getb(); // 获取成员 m_b 的值。
};
template <class T1, class T2>
T2 AA<T1,T2>::getb() // 获取成员 m_b 的值。
{
return m_b;
}
int main()
{
AA<int, string>* a = new AA<int, string>(3, "西施"); // 用模板类 AA 创建对象 a。
cout << "a->geta()=" << a->geta() << endl;
cout << "a->getb()=" << a->getb() << endl;
delete a;
}