C++学习笔记(4)

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 派生类名:[继承方式]基类名

{

派生类新增加的成员

};

被继承的类称为基类或父类,继承的类称为派生类或子类。

继承和派生是一个概念,只是站的角度不同。

派生类除了拥有基类的成员,还可以定义新的成员,以增强其功能。

使用继承的场景:

  1. 如果新创建的类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承。

  2. 当需要创建多个类时,如果它们拥有很多相似的成员变量或成员函数,可以将这些类共同的成员

提取出来,定义为基类,然后从基类继承。

示例:

#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 是用来指明基类成员在派生类中的最高访问

权限的。

  1. 不管继承方式如何,基类中的 private 成员在派生类中始终不能使用(不能在派生类的成员函数

中访问或调用)。

  1. 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为 public

或 protected;只有那些不希望在派生类中使用的成员才声明为 private。

  1. 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明

为 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;

}

相关推荐
捕鲸叉19 分钟前
创建线程时传递参数给线程
开发语言·c++·算法
A charmer24 分钟前
【C++】vector 类深度解析:探索动态数组的奥秘
开发语言·c++·算法
Peter_chq26 分钟前
【操作系统】基于环形队列的生产消费模型
linux·c语言·开发语言·c++·后端
aloha_7891 小时前
从零记录搭建一个干净的mybatis环境
java·笔记·spring·spring cloud·maven·mybatis·springboot
青花瓷2 小时前
C++__XCode工程中Debug版本库向Release版本库的切换
c++·xcode
dsywws2 小时前
Linux学习笔记之vim入门
linux·笔记·学习
晨曦_子画3 小时前
3种最难学习和最容易学习的 3 种编程语言
学习
幺零九零零3 小时前
【C++】socket套接字编程
linux·服务器·网络·c++
城南vision3 小时前
Docker学习—Docker核心概念总结
java·学习·docker
捕鲸叉3 小时前
MVC(Model-View-Controller)模式概述
开发语言·c++·设计模式