101、从结构体到类
对面向对象编程来说,一切都是对象,对象用类来描述。
类把对象的数据和操作数据的方法作为一个整体考虑。
定义类的语法:
class 类名
{
public:
成员一的数据类型 成员名一;
成员二的数据类型 成员名二;
成员三的数据类型 成员名三;
......
成员n的数据类型 成员名n;
};
注意:
- 类的成员可以是变量,也可以是函数。
- 类的成员变量也叫属性。
- 类的成员函数也叫方法/行为,类的成员函数可以定义在类的外面。
- 用类定义一个类的变量叫创建(或实例化)一个对象。
- 对象的成员变量和成员函数的作用域和生命周期与对象的作用域和生命周期相同。
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
struct st_girl // 超女基本信息结构体st_girl,存放了超女全部的数据项。
{
string name; // 姓名。
int age; // 年龄。
int height; // 身高(cm)。
double weight; // 体重(kg)。
char sex='X'; // 性别:X-女;Y-男。
int yz; // 颜值:1-漂亮;2-一般;3-歪瓜裂枣。
string special; // 特长。
string memo; // 备注。
};
void setvalue(st_girl& girl, string name, int age, int height, double weight,
char sex, int yz, string special, string memo)
{
girl.name = name;
girl.age = age;
girl.height = height;
girl.weight = weight;
girl.sex = sex;
girl.yz = yz;
girl.special = special;
girl.memo = memo;
}
void show(const st_girl& girl)
{
cout << "姓名:" << girl.name << ",年龄:" << girl.age << ",身高:" << girl.height
<< ",体重:" << girl.weight << ",性别:" << girl.sex << ",颜值:" << girl.yz
<< ",特长:" << girl.special << ",备注:" << girl.memo << endl;
}
int main()
{
st_girl girl;
setvalue(girl, "西施", 26, 170, 50.5, 'X', 1, "唱歌、跳舞、洗衣服。", "春秋第一美女,四大美女之一。");
show(girl);
}
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
struct st_girl // 超女基本信息结构体st_girl,存放了超女全部的数据项。
{
string name; // 姓名。
int age; // 年龄。
void setvalue(string name1, int age1) // 设置成员变量的值。
{
name = name1; age = age1;
}
void show() // 显示超女的自我介绍。
{
cout << "姓名:" << name << ",年龄:" << age << endl;
}
};
int main()
{
st_girl girl; // 创建结构体变量。
girl.setvalue("西施", 26); // 设置成员变量的值。
girl.show(); // 显示超女的自我介绍。
}
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
public:
string name; // 姓名。
int age; // 年龄。
void setvalue(string name1, int age1); // 设置成员变量的值。
void show() // 显示超女的自我介绍。
{
cout << "姓名:" << name << ",年龄:" << age << endl;
}
};
void CGirl::setvalue(string name1, int age1) // 设置成员变量的值。
{
name = name1; age = age1;
}
int main()
{
CGirl girl; // 创建超女对象。
girl.setvalue("西施", 26); // 设置成员变量的值。
girl.show(); // 显示超女的自我介绍。
}
102、类的访问权限
类的成员有三种访问权限:public、private和protected,分别表示公有的、私有的和受保护的。
在类的内部(类的成员函数中),无论成员被声明为 public还是private,都可以访问。
在类的外部(定义类的代码之外),只能访问public成员,不能访问 private、protected成员。
在一个类体的定义中,private 和 public 可以出现多次。
结构体的成员缺省为public,类的成员缺省为private。
private的意义在于隐藏类的数据和实现,把需要向外暴露的成员声明为public。
103、简单使用类
编程思想和方法的改变,披着C++外衣的C程序员。
1)类的成员函数可以直接访问该类其它的成员函数(可以递归)。
2)类的成员函数可以重载,可以使用默认参数。
3)类指针的用法与结构体指针用法相同。
4)类的成员可以是任意数据类型(类中枚举)。
5)可以为类的成员指定缺省值(C++11标准)。
6)类可以创建对象数组,就像结构体数组一样。
7)对象可以作为实参传递给函数,一般传引用。
8)可以用new动态创建对象,用delete释放对象。
9)在类的外部,一般不直接访问(读和写)对象的成员,而是用成员函数。数据隐藏是面向对象编程的思想之一。
10)对象一般不用memset()清空成员变量,可以写一个专用于清空成员变量的成员函数。
11)对类和对象用sizeof运算意义不大,一般不用。
12)用结构体描述纯粹的数据,用类描述对象。
13)在类的声明中定义的函数都将自动成为内联函数;在类的声明之外定义的函数如果使用了inline限定符,也是内联函数。
14)为了区分类的成员变量和成员函数的形参,把成员变量名加m_前缀或_后缀,如m_name或name_。
15)类的分文件编写。
104、构造函数和析构函数
构造函数:在创建对象时,自动的进行初始化工作。
析构函数:在销毁对象前,自动的完成清理工作。
1)构造函数
语法:类名(){......}
- 访问权限必须是public。
- 函数名必须与类名相同。
- 没有返回值,不写void。
- 可以有参数,可以重载,可以有默认参数。
- 创建对象时只会自动调用一次,不能手工调用。
2)析构函数
语法:~类名(){......}
- 访问权限必须是public。
- 函数名必须在类名前加~。
- 没有返回值,也不写void。
- 没有参数,不能重载。
- 销毁对象前只会自动调用一次,但是可以手工调用。
注意:
- 如果没有提供构造/析构函数,编译器将提供空实现的构造/析构函数。
- 如果提供了构造/析构函数,编译器将不提供空实现的构造/析构函数。
- 创建对象的时候,如果重载了构造函数,编译器根据实参匹配相应的构造函数。没有参数的构造函数也叫默认构造函数。
- 创建对象的时候不要在对象名后面加空的圆括号,编译器误认为是声明函数。(如果没有构造函数、构造函数没有参数、构造函数的参数都有默认参数)
- 在构造函数名后面加括号和参数不是调用构造函数,是创建匿名对象。
- 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值(可能会导致问题,不推荐)。
CGirl girl =10;
- 以下两行代码有本质的区别:
CGirl girl = CGirl("西施"20); // 显式创建对象。
CGirl girl; // 创建对象。
girl = CGirl("西施"20); // 创建匿名对象,然后给现有的对象赋值。
- 用new/delete创建/销毁对象时,也会调用构造/析构函数。
- 不建议在构造/析构函数中写太多的代码,可以调用成员函数。
- 除了初始化,不建议让构造函数做太多工作(只能成功不会失败)。
- C++11支持使用统一初始化列表。
CGirl girl = {"西施"20};
CGirl girl {"西施"20};
CGirl* girl = new CGirl{ "西施"20 };
- 如果类的成员也是类,创建对象的时候,先构造成员类;销毁对象的时候,先析构自身,再析构成员类(视频中有误)。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
public:
string m_name; // 姓名属性。
int m_age; // 年龄属性。
char m_memo[301]; // 备注。
CGirl() // 没有参数的构造函数。
{
initdata();
cout << "调用了CGirl()构造函数。\n";
}
CGirl(string name) // 一个参数(姓名)的构造函数。
{
initdata();
cout << "调用了CGirl(name)构造函数。\n";
m_name = name;
}
CGirl(int age) // 一个参数(年龄)的构造函数。
{
initdata();
cout << "调用了CGirl(age)构造函数。\n";
m_age = age;
}
CGirl(string name, int age) // 两个参数的构造函数。
{
initdata();
cout << "调用了CGirl(name,age)构造函数。\n";
m_name = name; m_age = age;
}
void initdata()
{
m_name.clear(); m_age = 0; memset(m_memo, 0, sizeof(m_memo));
}
~CGirl() // 析构函数。
{
cout << "调用了~CGirl()\n";
}
void show() // 超女自我介绍的方法。
{ cout << "姓名:" << m_name << ",年龄:" << m_age << ",备注:" << m_memo<< endl; }
};
int main()
{
// CGirl girl; // 创建超女对象,不设置任何初始值。
// CGirl girl("西施"); // 创建超女对象,为成员姓名设置初始值。
// CGirl girl("西施",8); // 创建超女对象,为成员姓名和年龄设置初始值。
// CGirl girl=CGirl(); // 创建超女对象,不设置任何初始值。
// CGirl girl=CGirl("西施"); // 创建超女对象,为成员姓名设置初始值。
// CGirl girl=CGirl("西施",8); // 创建超女对象,为成员姓名和年龄设置初始值。
// CGirl girl = 8; // 使用赋值语法初始化对象。
// CGirl *girl=new CGirl; // 创建超女对象,不设置任何初始值。
// CGirl *girl=new CGirl("西施"); // 创建超女对象,为成员姓名设置初始值。
CGirl *girl=new CGirl("西施",8); // 创建超女对象,为成员姓名和年龄设置初始值。
girl->show(); // 显示超女的自我介绍。
delete girl;
}
105、拷贝构造函数
用一个已存在的对象创建新的对象,不会调用(普通)构造函数,而是调用拷贝构造函数。
如果类中没有定义拷贝构造函数,编译器将提供一个拷贝构造函数,它的功能是把已存在对象的成员变量赋值给新对象的成员变量。
用一个已存在的对象创建新的对象语法:
类名 新对象名(已存在的对象名);
类名 新对象名=已存在的对象名;
拷贝构造函数的语法:
类名(const 类名& 对象名){......}
注意:
- 访问权限必须是public。
- 函数名必须与类名相同。
- 没有返回值,不写void。
- 如果类中定义了拷贝构造函数,编译器将不提供默认的拷贝构造函数。
- 以值传递的方式调用函数时,如果实参为对象,会调用拷贝构造函数。
- 函数以值的方式返回对象时,可能会调用拷贝构造函数(VS会调用,Linux不会,g++编译器做了优化)。
- 拷贝构造函数可以重载,可以有默认参数。
类名(......,const 类名& 对象名,......){......}
- 如果类中重载了拷贝构造函数却没有定义默认的拷贝构造函数,编译器也会提供默认的拷贝构造函数。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
public:
string m_name; // 姓名属性。
int m_age; // 年龄属性。
// 没有参数的普通构造函数。
CGirl() { m_name.clear(); m_age = 0; cout << "调用了CGirl()构造函数。\n"; }
// 没有重载的拷贝构造函数(默认拷贝构造函数)。
CGirl(const CGirl &gg) { m_name="漂亮的"+gg.m_name; m_age = gg.m_age-1; cout << "调用了CGirl(const CGirl &gg)拷贝构造函数。\n"; }
// 重载的拷贝构造函数。
CGirl(const CGirl& gg,int ii) { m_name = "漂亮的" + gg.m_name; m_age = gg.m_age - ii; cout << "调用了CGirl(const CGirl &gg,int ii)拷贝构造函数。\n"; }
// 析构函数。
~CGirl() { cout << "调用了~CGirl()\n"; }
// 超女自我介绍的方法,显示姓名和年龄。
void show() { cout << "姓名:" << m_name << ",年龄:" << m_age << endl; }
};
int main()
{
CGirl g1;
g1.m_name = "西施"; g1.m_age = 23;
CGirl g2(g1,3);
g2.show();
}
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
public:
string m_name; // 姓名属性。
int m_age; // 年龄属性。
int* m_ptr; // 指针成员,计划使用堆内存。
// 没有参数的普通构造函数。
CGirl() { m_name.clear(); m_age = 0; m_ptr = nullptr; cout << "调用了CGirl()构造函数。\n"; }
// 没有重载的拷贝构造函数(默认拷贝构造函数)。
CGirl(const CGirl& gg)
{
m_name = gg.m_name; m_age = gg.m_age;
m_ptr = new int; // 分配内存。
// *m_ptr = *gg.m_ptr; // 拷贝数据。
memcpy(m_ptr, gg.m_ptr, sizeof(int)); // 拷贝数据。
cout << "调用了CGirl(const CGirl &gg)拷贝构造函数。\n";
}
// 析构函数。
~CGirl() { delete m_ptr; m_ptr = nullptr; cout << "调用了~CGirl()\n"; }
// 超女自我介绍的方法,显示姓名和年龄。
void show() { cout << "姓名:" << m_name << ",年龄:" << m_age << ",m_ptr="<< m_ptr<<",*m_ptr="<<*m_ptr<<endl; }
};
int main()
{
CGirl g1;
g1.m_name = "西施"; g1.m_age = 23; g1.m_ptr = new int(3);
g1.show();
CGirl g2(g1); *g2.m_ptr = 8;
g1.show();
g2.show();
}
106、初始化列表
构造函数的执行可以分成两个阶段:初始化阶段和计算阶段(初始化阶段先于计算阶段)。
构造函数除了参数列表和函数体之外,还可以有初始化列表。
初始化列表的语法:
类名(形参列表):成员一(值一), 成员二(值二),..., 成员n(值n)
{......}
注意:
1)如果成员已经在初始化列表中,则不应该在构造函数中再次赋值。
2)初始化列表的括号中可以是具体的值,也可以是构造函数的形参名,还可以是表达式。
3)初始化列表与赋值有本质的区别,如果成员是类,使用初始化列表调用的是成员类的拷贝构造函数,而赋值则是先创建成员类的对象(将调用成员类的普通构造函数),然后再赋值。
4)如果成员是类,初始化列表对性能略有提升。
5)如果成员是常量和引用,必须使用初始列表,因为常量和引用只能在定义的时候初始化。
6)如果成员是没有默认构造函数的类,则必须使用初始化列表。
7)拷贝构造函数也可以有初始化列表。
8)类的成员变量可以不出现在初始化列表中。
9)构造函数的形参先于成员变量初始化。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CBoy // 男朋友类。
{
public:
string m_xm; // 男朋友的姓名。
CBoy() // 没有参数的普通构造函数,默认构造函数。
{ m_xm.clear(); cout << "调用了CBoy()构造函数。\n"; }
CBoy(string xm) // 有一个参数的普通构造函数。
{ m_xm = xm; cout << "调用了CBoy(string xm)构造函数。\n"; }
CBoy(const CBoy& bb) // 默认拷贝构造函数。
{ m_xm = bb.m_xm; cout << "调用了CBoy(const CBoy &bb)拷贝构造函数。\n"; }
};
class CGirl // 超女类CGirl。
{
public:
string m_name; // 姓名属性。
const int m_age; // 年龄属性。
CBoy& m_boy; // 男朋友的信息。
//CGirl() // 没有参数的普通构造函数,默认构造函数。
//{
// cout << "调用了CGirl()构造函数。\n";
//}
//CGirl(string name, int age,CBoy &boy) // 三个参数的普通构造函数。
//{
// m_name = name; m_age = age; m_boy.m_xm = boy.m_xm;
// cout << "调用了CGirl(name,age,boy)构造函数。\n";
//}
CGirl(string name, int age, CBoy& boy) :m_name(name), m_age(age),m_boy(boy) // 三个参数的普通构造函数。
{
cout << "调用了CGirl(name,age,boy)构造函数。\n";
}
// 超女自我介绍的方法,显示姓名、年龄、男朋友。
void show() { cout << "姓名:" << m_name << ",年龄:" << m_age << ",男朋友:" << m_boy.m_xm << endl; }
};
int main()
{
CBoy boy("子都");
CGirl g1("冰冰",18,boy);
g1.show();
}
107、const修饰成员函数
在类的成员函数后面加const关键字,表示在成员函数中保证不会修改调用对象的成员变量。
注意:
1)mutable可以突破const的限制,被mutable修饰的成员变量,将永远处于可变的状态,在const修饰的函数中,mutable成员也可以被修改。
2)非const成员函数可以调用const成员函数和非const成员函数。
3)const成员函数不能调用非const成员函数。
4)非const对象可以调用const修饰的成员函数和非const修饰的成员函数。
5)const对象只能调用const修饰的成员函数,不能调用非cosnt修饰的成员函数。
这里出现了令人纠结的三个问题:
1、为什么要保护类的成员变量不被修改?
2、为什么用const保护了成员变量,还要再定义一个mutable关键字来突破const的封锁线?
3、到底有没有必要使用const和mutable这两个关键字?
保护类的成员变量不在成员函数中被修改,是为了保证模型的逻辑正确,通过用const关键字来避免在函数中错误的修改了类对象的状态。并且在所有使用该成员函数的地方都可以更准确的预测到使用该成员函数的带来的影响。而mutable则是为了能突破const的封锁线,让类的一些次要的或者是辅助性的成员变量随时可以被更改。没有使用const和mutable关键字当然没有错,const和mutable 关键字只是给了建模工具更多的设计约束和设计灵活性,而且程序员也可以把更多的逻辑检查问题交给编译器和建模工具去做,从而减轻程序员的负担。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
public:
mutable string m_name; // 姓名属性。
int m_age; // 年龄属性。
// 两个参数的普通构造函数。
CGirl(const string &name, int age)
{ m_name = name; m_age = age; cout << "调用了CGirl(name,age)构造函数。\n"; }
// 超女自我介绍的方法,显示姓名、年龄。
void show1() const
{
m_name="西施show1";
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
void show2() const
{
m_name = "西施show2";
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
void show3()
{
m_name = "西施show3";
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
void show4()
{
m_name = "西施show4";
cout << "姓名:" << m_name << ",年龄:" << m_age << endl;
}
};
int main()
{
const CGirl g1("冰冰",18);
g1.show1();
}
108、this指针
如果类的成员函数中涉及多个对象,在这种情况下需要使用this指针。
this指针存放了对象的地址,它被作为隐藏参数传递给了成员函数,指向调用成员函数的对象(调用者对象)。
每个成员函数(包括构造函数和析构函数)都有一个this指针,可以用它访问调用者对象的成员。(可以解决成员变量名与函数形参名相同的问题)
*this可以表示对象。
如果在成员函数的括号后面使用const,那么将不能通过this指针修改成员变量。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
public:
string m_name; // 姓名属性。
int m_yz; // 颜值:1-沉鱼落雁;2-漂亮;3-一般;4-歪瓜裂枣。
// 两个参数的普通构造函数。
CGirl(const string &name, int yz) { m_name = name; m_yz = yz; }
// 超女自我介绍的方法。
void show() const { cout << "我是:" << m_name << ",最漂亮的超女。"<< endl; }
// 超女颜值pk的方法。
const CGirl& pk(const CGirl& g) const
{
if (g.m_yz < m_yz) return g;
return *this;
}
};
int main()
{
// 比较五个超女的颜值,然后由更漂亮的超女作自我介绍。
CGirl g1("西施",5), g2("西瓜",3), g3("冰冰", 4), g4("幂幂", 5), g5("金莲", 2);
const CGirl& g = g1.pk(g2).pk(g3).pk(g4).pk(g5);
g.show();
}
109、静态成员
类的静态成员包括静态成员变量和静态成员函数。
用静态成员可以变量实现多个对象之间的数据共享,比全局变量更安全性。
用 static 关键字把类的成员变量声明为静态,表示它在程序中(不仅是对象)是共享的。
静态成员变量不会在创建对象的时候初始化,必须在程序的全局区用代码清晰的初始化(用范围解析运算符 ::)。
静态成员使用类名加范围解析运算符 :: 就可以访问,不需要创建对象。
如果把类的成员声明为静态的,就可以把它与类的对象独立开来(静态成员不属于对象)。
静态成员变量在程序中只有一份(生命周期与程序运行期相同,存放在静态存储区的),不论是否创建了类的对象,也不论创建了多少个类的对象。
在静态成员函数中,只能访问静态成员,不能访问非静态成员。
静态成员函数中没有this指针。
在非静态成员函数中,可以访问静态成员。
私有静态成员在类外无法访问。
const静态成员变量可以在定义类的时候初始化。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
static int m_age; // 年龄属性。
public:
string m_name; // 姓名属性。
// 两个参数的普通构造函数。
CGirl(const string& name, int age) { m_name = name; m_age = age; }
// 显示超女的姓名。
void showname() { cout << "姓名:" << m_name << endl; }
// 显示超女的年龄。
static void showage() { cout << "年龄:" << m_age << endl; }
};
int CGirl::m_age=8; // 初始化类的静态成员变量。
int main()
{
CGirl g1("西施1", 21), g2("西施2", 22), g3("西施3", 23);
g1.showname(); g1.showage();
g2.showname(); g2.showage();
g3.showname(); g3.showage();
CGirl::showage();
// cout << "CGirl::m_age=" << CGirl::m_age << endl;
}
110、简单对象模型
在C语言中,数据和处理数据的操作(函数)是分开的。也就是说,C语言本身没有支持数据和函数之间的关联性。
C++用类描述抽象数据类型(abstract data type,ADT),在类中定义了数据和函数,把数据和函数关联起来。
对象中维护了多个指针表,表中放了成员与地址的对应关系。
class CGirl // 超女类CGirl。
{
public:
char m_name[10]; // 姓名属性。
int m_age; // 年龄属性。
// 默认构造函数和析构函数。
CGirl() { memset(m_name, 0, sizeof(m_name)); m_age = 0; }
~CGirl() { }
// 显示超女的姓名。
void showname() { cout << "姓名:" << m_name << endl; }
// 显示超女的年龄。
void showage() { cout << "年龄:" << m_age << endl; }
};
C++类中有两种数据成员:nonstatic、static,三种函数成员:nonstatic、static、virtual。
- 对象内存的大小包括:1)所有非静态数据成员的大小;2)由内存对齐而填补的内存大小;3)为了支持virtual成员而产生的额外负担。
- 静态成员变量属于类,不计算在对象的大小之内。
- 成员函数是分开存储的,不论对象是否存在都占用存储空间,在内存中只有一个副本,也不计算在对象大小之内。
- 用空指针可以调用没有用到this指针的非静态成员函数。
- 对象的地址是第一个非静态成员变量的地址,如果类中没有非静态成员变量,编译器会隐含的增加一个1字节的占位成员。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
public:
char m_name[3]; // 姓名属性。
int m_bh; // 编号属性。
static int m_age; // 年龄属性。
// 默认构造函数和析构函数。
CGirl() { memset(m_name, 0, sizeof(m_name)); m_age = 0; }
~CGirl() { }
// 显示超女的姓名。
void showname() { if (this == nullptr) return; cout << "姓名:" << this->m_name << endl; }
// 显示超女的年龄。
void showage() { cout << "年龄:" << m_age << endl; }
};
int CGirl::m_age;
int aaa;
void func() {}
int main()
{
CGirl g;
cout << "对象g占用的内存大小是:" << sizeof(g) << endl;
cout << "对象g的地址是:" << (void*)&g << endl;
cout << "成员变量m_bh的地址是:" << (void*)&g.m_bh << endl;
cout << "成员变量m_name的地址是:" << (void*)&g.m_name << endl;
cout << "成员变量m_age的地址是:" << (void *)&g.m_age << endl;
cout << "全局变量aaa的地址是:" << (void*)&aaa << endl;
printf("成员函数showname的地址是:%p\n", &CGirl::showname);
printf("成员函数showage的地址是:%p\n", &CGirl::showage);
printf("函数func()的地址是:%p\n", func);
CGirl* g1 = nullptr;
g1->showname();
}
111、友元
如果要访问类的私有成员变量,调用类的公有成员函数是唯一的办法,而类的私有成员函数则无法访问。
友元提供了另一访问类的私有成员的方案。友元有三种:
- 友元全局函数。
- 友元类。
- 友元成员函数。
1)友元全局函数
在友元全局函数中,可以访问另一个类的所有成员。
2)友元类
在友元类所有成员函数中,都可以访问另一个类的所有成员。
友元类的注意事项:
- 友元关系不能被继承。
- 友元关系是单向的,不具备交换性。
若类B是类A的友元,类A不一定是类B的友元。B是类A的友元,类C是B的友元,类C不一定是类A的友元,要看类中是否有相应的声明。
3)友元成员函数
在友元成员函数中,可以访问另一个类的所有成员。
如果要把男朋友类CBoy的某成员函数声明为超女类CGirl的友元,声明和定义的顺序如下:
class CGirl; // 前置声明。
class CBoy { ...... }; // CBoy的定义。
class CGirl { ...... }; // CGirl的定义。
// 友元成员函数的定义。
void CBoy::func(CGirl &g) { ...... }
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
friend int main();
friend void func();
public:
string m_name; // 姓名。
// 默认构造函数。
CGirl() { m_name = "西施"; m_xw = 87; }
// 显示姓名的成员函数。
void showname() { cout << "姓名:" << m_name << endl; }
private:
int m_xw; // 胸围。
// 显示胸围的成员函数。
void showxw() { cout << "胸围:" << m_xw << endl; }
};
void func()
{
CGirl g;
g.showname();
g.showxw();
}
int main()
{
func();
}
///////////////////////////////////////////////////////////////////////////////////////
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
friend class CBoy;
public:
string m_name; // 姓名。
// 默认构造函数。
CGirl() { m_name = "西施"; m_xw = 87; }
// 显示姓名的成员函数。
void showname() { cout << "姓名:" << m_name << endl; }
private:
int m_xw; // 胸围。
// 显示胸围的成员函数。
void showxw() const { cout << "胸围:" << m_xw << endl; }
};
class CBoy // 超女的男朋友类
{
public:
void func(const CGirl& g)
{
cout << "我女朋友的姓名是:" << g.m_name << endl;
cout << "我女朋友的胸围是:" << g.m_xw << endl;
g.showxw();
}
};
int main()
{
CGirl g;
CBoy b;
b.func(g);
}
///////////////////////////////////////////////////////////////////////////////////////
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl; // 把超女类的声明前置
class CBoy // 超女的男朋友类
{
public:
void func1(const CGirl& g);
void func2(const CGirl& g);
};
class CGirl // 超女类CGirl。
{
friend void CBoy::func1(const CGirl& g);
// friend void CBoy::func2(const CGirl& g);
public:
string m_name; // 姓名。
// 默认构造函数。
CGirl() { m_name = "西施"; m_xw = 87; }
// 显示姓名的成员函数。
void showname() { cout << "姓名:" << m_name << endl; }
private:
int m_xw; // 胸围。
// 显示胸围的成员函数。
void showxw() const { cout << "胸围:" << m_xw << endl; }
};
void CBoy::func1(const CGirl& g) { cout << "func1()我女朋友的胸围是:" << g.m_xw << endl; }
void CBoy::func2(const CGirl& g) { cout << "func2()我女朋友的姓名是:" << g.m_name << endl; }
int main()
{
CGirl g;
CBoy b;
b.func2(g);
b.func1(g);
}
112、运算符重载基础
C++将运算符重载扩展到自定义的数据类型,它可以让对象操作更美观。
例如字符串string用加号(+)拼接、cout用两个左尖括号(<<)输出。
运算符重载函数的语法:返回值 operator运算符(参数列表);
运算符重载函数的返回值类型要与运算符本身的含义一致。
非成员函数版本的重载运算符函数:形参个数与运算符的操作数个数相同;
成员函数版本的重载运算符函数:形参个数比运算符的操作数个数少一个,其中的一个操作数隐式传递了调用对象。
如果同时重载了非成员函数和成员函数版本,会出现二义性。
注意:
1)返回自定义数据类型的引用可以让多个运算符表达式串联起来。(不要返回局部变量的引用)
2)重载函数参数列表中的顺序决定了操作数的位置。
3)重载函数的参数列表中至少有一个是用户自定义的类型,防止程序员为内置数据类型重载运算符。
4)如果运算符重载既可以是成员函数也可以是全局函数,应该优先考虑成员函数,这样更符合运算符重载的初衷。
5)重载函数不能违背运算符原来的含义和优先级。
6)不能创建新的运算符。
7)以下运算符不可重载:
- sizeof sizeof运算符
- . 成员运算符
- .* 成员指针运算符
- :: 作用域解析运算符
- ?: 条件运算符
- typeid 一个RTTI运算符
- const_cast 强制类型转换运算符
- dynamic_cast 强制类型转换运算符
- reinterpret_cast 强制类型转换运算符
- static_cast 强制类型转换运算符
8)以下运算符只能通过成员函数进行重载:
- = 赋值运算符
- () 函数调用运算符
- [] 下标运算符
- -> 通过指针访问类成员的运算符
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
friend CGirl &operator+(CGirl& g, int score);
friend CGirl& operator+(int score, CGirl& g);
friend CGirl& operator+(CGirl& g1, CGirl& g2);
private:
int m_xw; // 胸围。
int m_score; // 分数。
public:
string m_name; // 姓名。
// 默认构造函数。
CGirl() { m_name = "西施"; m_xw = 87; m_score = 30; }
// 自我介绍的方法。
void show() { cout << "姓名:" << m_name << ",胸围:" << m_xw << ",评分:" << m_score << endl; }
//CGirl& operator-(int score) // 给超女减分的函数。
//{
// m_score = m_score - score;
// return *this;
//}
};
CGirl& operator+(CGirl& g, int score) // 给超女加分的函数。
{
g.m_score = g.m_score + score;
return g;
}
CGirl& operator+(int score,CGirl& g) // 给超女加分的函数。
{
g.m_score = g.m_score + score;
return g;
}
CGirl& operator+(CGirl& g1, CGirl& g2) // 给超女加分的函数。
{
g1.m_score = g1.m_score + g2.m_score;
return g1;
}
int main()
{
// 导演的要求:每轮表演之后,给超女加上她的得分。
CGirl g;
g = g+g;
g.show();
}
113、重载关系运算符
重载关系运算符(==、!=、>、>=、<、<=)用于比较两个自定义数据类型的大小。
可以使用非成员函数和成员函数两种版本,建议采用成员函数版本。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
string m_name; // 姓名。
int m_yz; // 颜值:1-千年美人;2-百年美人;3-绝代美人;4-极漂亮;5-漂亮;6-一般;7-歪瓜裂枣。
int m_sc; // 身材:1-火辣;2-...;3-...;4-...;5-...;6-...;7-膘肥体壮。
int m_acting; // 演技:1-完美;2-...;3-...;4-...;5-...;6-...;7-四不像。
public:
// 四个参数的构造函数。
CGirl(string name, int yz, int sc, int acting) { m_name = name; m_yz = yz; m_sc = sc; m_acting = acting; }
// 比较两个超女的商业价值。
bool operator==(const CGirl& g1) // 相等==
{
if ((m_yz + m_sc + m_acting) == (g1.m_yz + g1.m_sc + g1.m_acting)) return true;
return false;
}
bool operator>(const CGirl& g1) // 大于>
{
if ((m_yz + m_sc + m_acting) < (g1.m_yz + g1.m_sc + g1.m_acting)) return true;
return false;
}
bool operator<(const CGirl& g1) // 小于<
{
if ((m_yz + m_sc + m_acting) > (g1.m_yz + g1.m_sc + g1.m_acting)) return true;
return false;
}
};
int main()
{
CGirl g1("西施", 1, 2, 2), g2("冰冰", 1, 1, 1);
if (g1==g2)
cout << "西施和冰冰的商业价值相同。\n";
else
if (g1>g2)
cout << "西施商业价值相同比冰冰大。\n";
else
cout << "冰冰商业价值相同比西施大。\n";
}
114、重载左移运算符
重载左移运算符(<<)用于输出自定义对象的成员变量,在实际开发中很有价值(调试和日志)。
只能使用非成员函数版本。
如果要输出对象的私有成员,可以配合友元一起使用。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
friend ostream& operator<<(ostream& cout, const CGirl& g);
string m_name; // 姓名。
int m_xw; // 胸围。
int m_score; // 评分。
public:
// 默认构造函数。
CGirl() { m_name = "西施"; m_xw = 87; m_score = 30; }
// 自我介绍的方法。
void show() { cout << "姓名:" << m_name << ",胸围:" << m_xw << ",评分:" << m_score << endl; }
};
ostream& operator<<(ostream& cout, const CGirl& g)
{
cout << "姓名:" << g.m_name << ",胸围:" << g.m_xw << ",评分:" << g.m_score;
return cout;
}
int main()
{
CGirl g;
cout << g << endl;
}
115、重载下标运算符
如果对象中有数组,重载下标运算符[],操作对象中的数组将像操作普通数组一样方便。
下标运算符必须以成员函数的形式进行重载。
下标运算符重载函数的语法:
返回值类型 &perator[](参数);
或者:
const 返回值类型 &operator[](参数) const;
使用第一种声明方式,[]不仅可以访问数组元素,还可以修改数组元素。
使用第二种声明方式,[]只能访问而不能修改数组元素。
在实际开发中,我们应该同时提供以上两种形式,这样做是为了适应const对象,因为通过const 对象只能调用const成员函数,如果不提供第二种形式,那么将无法访问const对象的任何数组元素。
在重载函数中,可以对下标做合法性检查,防止数组越界。
例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
private:
string m_boys[3]; // 超女的男朋友
public:
string m_name; // 姓名。
// 默认构造函数。
CGirl() { m_boys[0] = "子都"; m_boys[1] = "潘安"; m_boys[2] = "宋玉"; }
// 显示全部男朋友的姓名。
void show() { cout << m_boys[0] << "、" << m_boys[1] << "、" << m_boys[2] << endl; }
string& operator[](int ii)
{
return m_boys[ii];
}
const string& operator[](int ii) const
{
return m_boys[ii];
}
};
int main()
{
CGirl g; // 创建超女对象。
g[1] = "王麻子";
cout << "第1任男朋友:" << g[1] << endl;
g.show();
const CGirl g1 = g;
cout << "第1任男朋友:" << g1[1] << endl;
}
116、重载赋值运算符
C++编译器可能会给类添加四个函数:
- 默认构造函数,空实现。
- 默认析构函数,空实现。
- 默认拷贝构造函数,对成员变量进行浅拷贝。
- 默认赋值函数, 对成员变量进行浅拷贝。
对象的赋值运算是用一个已经存在的对象,给另一个已经存在的对象赋值。
如果类的定义中没有重载赋值函数,编译器就会提供一个默认赋值函数。
如果类中重载了赋值函数,编译器将不提供默认赋值函数。
重载赋值函数的语法:类名 & operator=(const 类名 & 源对象);
注意:
- 编译器提供的默认赋值函数,是浅拷贝。
- 如果对象中不存在堆区内存空间,默认赋值函数可以满足需求,否则需要深拷贝。
- 赋值运算和拷贝构造不同:拷贝构造是指原来的对象不存在,用已存在的对象进行构造;赋值运算是指已经存在了两个对象,把其中一个对象的成员变量的值赋给另一个对象的成员变量。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
public:
int m_bh; // 编号。
string m_name; // 姓名。
int* m_ptr; // 计划使用堆区内存。
CGirl() { m_ptr = nullptr; }
~CGirl() { if (m_ptr) delete m_ptr; }
// 显示全部成员变量。
void show() { cout << "编号:" << m_bh << ",姓名:" << m_name << ",m_ptr=" << m_ptr <</* ",*m_ptr=" << *m_ptr<< */endl; }
CGirl& operator=(const CGirl& g)
{
if (this == &g) return *this; // 如果是自己给自己赋值。
if (g.m_ptr == nullptr) // 如果源对象的指针为空,则清空目标对象的内存和指针。
{
if (m_ptr != nullptr) { delete m_ptr; m_ptr = nullptr; }
}
else // 如果源对象的指针不为空。
{
// 如果目标对象的指针为空,先分配内存。
if (m_ptr == nullptr) m_ptr = new int;
// 然后,把源对象内存中的数据复制到目标对象的内存中。
memcpy(m_ptr, g.m_ptr, sizeof(int));
}
m_bh = g.m_bh; m_name = g.m_name;
cout << "调用了重载赋值函数。\n" << endl;
return *this;
}
};
int main()
{
CGirl g1, g2; // 创建超女对象。
g1.m_bh = 8; g1.m_name = "西施"; g1.m_ptr = new int(3);
g1.show();
g2.show();
g2 = g1;
g2.show();
cout << "*g1.m_ptr=" << *g1.m_ptr << ",*g2.m_ptr=" << *g2.m_ptr << endl;
}
117、重载new&delete运算符
重载new和delete运算符的目是为了自定义内存分配的细节。(内存池:快速分配和归还,无碎片)
建议先学习C语言的内存管理函数malloc()和free()。
在C++中,使用new时,编译器做了两件事情:
1)调用标准库函数operator new()分配内存;
2)调用构造函数初始化内存;
使用delete时,也做了两件事情:
1)调用析构函数;
2)调用标准库函数operator delete()释放内存。
构造函数和析构函数由编译器调用,我们无法控制。
但是,可以重载内存分配函数operator new()和释放函数operator delete()。
1)重载内存分配函数的语法:void* operator new(size_t size);
参数必须是size_t,返回值必须是void*。
2)重载内存释放函数的语法:void operator delete(void* ptr)
参数必须是void *(指向由operator new()分配的内存),返回值必须是void。
重载的new和delete可以是全局函数,也可以是类的成员函数。
为一个类重载new和delete时,尽管不必显式地使用static,但实际上仍在创建static成员函数。
编译器看到使用new创建自定义的类的对象时,它选择成员版本的operator new()而不是全局版本的new()。
new[]和delete[]也可以重载。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void* operator new(size_t size) // 参数必须是size_t(unsigned long long),返回值必须是void*。
{
cout << "调用了全局重载的new:" << size << "字节。\n";
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) // 参数必须是void *,返回值必须是void。
{
cout << "调用了全局重载的delete。\n";
if (ptr == 0) return; // 对空指针delete是安全的。
free(ptr); // 释放内存。
}
class CGirl // 超女类CGirl。
{
public:
int m_bh; // 编号。
int m_xw; // 胸围。
CGirl(int bh, int xw) { m_bh = bh, m_xw = xw; cout << "调用了构造函数CGirl()\n"; }
~CGirl() { cout << "调用了析构函数~CGirl()\n"; }
void* operator new(size_t size) // 参数必须是size_t(unsigned long long),返回值必须是void*。
{
cout << "调用了类的重载的new:" << size << "字节。\n";
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) // 参数必须是void *,返回值必须是void。
{
cout << "调用了类的重载的delete。\n";
if (ptr == 0) return; // 对空指针delete是安全的。
free(ptr); // 释放内存。
}
};
int main()
{
int* p1 = new int(3);
cout << "p1=" << (void *)p1 <<",*p1=" <<*p1<< endl;
delete p1;
CGirl* p2 = new CGirl(3, 8);
cout << "p2的地址是:" << p2 << "编号:" << p2->m_bh << ",胸围:" << p2->m_xw << endl;
delete p2;
}
内存池示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
public:
int m_bh; // 编号。
int m_xw; // 胸围。
static char* m_pool; // 内存池的起始地址。
static bool initpool() // 个初始化内存池的函数。
{
m_pool = (char*)malloc(18); // 向系统申请18字节的内存。
if (m_pool == 0) return false; // 如果申请内存失败,返回false。
memset(m_pool, 0, 18); // 把内存池中的内容初始化为0。
cout << "内存池的起始地址是:" << (void*)m_pool << endl;
return true;
}
static void freepool() // 释放内存池。
{
if (m_pool == 0) return; // 如果内存池为空,不需要释放,直接返回。
free(m_pool); // 把内存池归还给系统。
cout << "内存池已释放。\n";
}
CGirl(int bh, int xw) { m_bh = bh, m_xw = xw; cout << "调用了构造函数CGirl()\n"; }
~CGirl() { cout << "调用了析构函数~CGirl()\n"; }
void* operator new(size_t size) // 参数必须是size_t(unsigned long long),返回值必须是void*。
{
if (m_pool[0] == 0) // 判断第一个位置是否空闲。
{
cout << "分配了第一块内存:" << (void*)(m_pool + 1) << endl;
m_pool[0] = 1; // 把第一个位置标记为已分配。
return m_pool + 1; // 返回第一个用于存放对象的址。
}
if (m_pool[9] == 0) // 判断第二个位置是否空闲。
{
cout << "分配了第二块内存:" << (void*)(m_pool + 9) << endl;
m_pool[9] = 1; // 把第二个位置标记为已分配。
return m_pool + 9; // 返回第二个用于存放对象的址。
}
// 如果以上两个位置都不可用,那就直接系统申请内存。
void* ptr = malloc(size); // 申请内存。
cout << "申请到的内存的地址是:" << ptr << endl;
return ptr;
}
void operator delete(void* ptr) // 参数必须是void *,返回值必须是void。
{
if (ptr == 0) return; // 如果传进来的地址为空,直接返回。
if (ptr == m_pool + 1) // 如果传进来的地址是内存池的第一个位置。
{
cout << "释放了第一块内存。\n";
m_pool[0] = 0; // 把第一个位置标记为空闲。
return;
}
if (ptr == m_pool + 9) // 如果传进来的地址是内存池的第二个位置。
{
cout << "释放了第二块内存。\n";
m_pool[9] = 0; // 把第二个位置标记为空闲。
return;
}
// 如果传进来的地址不属于内存池,把它归还给系统。
free(ptr); // 释放内存。
}
};
char* CGirl::m_pool = 0; // 初始化内存池的指针。
int main()
{
// 初始化内存池。
if (CGirl::initpool()==false) { cout << "初始化内存池失败。\n"; return -1; }
CGirl* p1 = new CGirl(3, 8); // 将使用内存池的第一个位置。
cout << "p1的地址是:" << p1 << ",编号:" << p1->m_bh << ",胸围:" << p1->m_xw << endl;
CGirl* p2 = new CGirl(4, 7); // 将使用内存池的第二个位置。
cout << "p2的地址是:" << p2 << ",编号:" << p2->m_bh << ",胸围:" << p2->m_xw << endl;
CGirl* p3 = new CGirl(6, 9); // 将使用系统的内存。
cout << "p3的地址是:" << p3 << ",编号:" << p3->m_bh << ",胸围:" << p3->m_xw << endl;
delete p1; // 将释放内存池的第一个位置。
CGirl* p4 = new CGirl(5, 3); // 将使用内存池的第一个位置。
cout << "p4的地址是:" << p4 << ",编号:" << p4->m_bh << ",胸围:" << p4->m_xw << endl;
delete p2; // 将释放内存池的第二个位置。
delete p3; // 将释放系统的内存。
delete p4; // 将释放内存池的第一个位置。
CGirl::freepool(); // 释放内存池。
}
118、重载括号运算符
括号运算符()也可以重载,对象名可以当成函数来使用(函数对象、仿函数)。
括号运算符重载函数的语法:
返回值类型 operator()(参数列表);
注意:
- 括号运算符必须以成员函数的形式进行重载。
- 括号运算符重载函数具备普通函数全部的特征。
- 如果函数对象与全局函数同名,按作用域规则选择调用的函数。
函数对象的用途:
1)表面像函数,部分场景中可以代替函数,在STL中得到广泛的应用;
2)函数对象本质是类,可以用成员变量存放更多的信息;
3)函数对象有自己的数据类型;
4)可以提供继承体系。
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
void show(string str) // 向超女表白的函数。
{
cout << "普通函数:" << str << endl;
}
class CGirl // 超女类。
{
public:
void operator()(string str) // 向超女表白的函数。
{
cout << "重载函数:" << str << endl;
}
};
int main()
{
CGirl show;
::show("我是一只傻傻鸟。");
show("我是一只傻傻鸟。");
}
119、重载一元运算符
可重载的一元运算符。
1)++ 自增 2)-- 自减 3)! 逻辑非 4)& 取地址
5)~ 二进制反码 6)* 解引用 7)+ 一元加 8) - 一元求反
一元运算符通常出现在它们所操作的对象的左边。
但是,自增运算符++和自减运算符--有前置和后置之分。
C++ 规定,重载++或--时,如果重载函数有一个int形参,编译器处理后置表达式时将调用这个重载函数。
成员函数版:CGirl &operator++(); // ++前置
成员函数版:CGirl operator++(int); // 后置++
非成员函数版:CGirl &operator++(CGirl &); // ++前置
非成员函数版:CGirl operator++(CGirl &,int); // 后置++
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
class CGirl // 超女类CGirl。
{
public:
string m_name; // 姓名。
int m_ranking; // 排名。
// 默认构造函数。
CGirl() { m_name = "西施"; m_ranking = 5; }
// 自我介绍的方法。
void show() const { cout << "姓名:" << m_name << ",排名:" << m_ranking << endl; }
CGirl & operator++() // ++前置的重载函数。
{
m_ranking++; return *this;
}
CGirl operator++(int) // ++后置的重载函数。
{
CGirl tmp = *this;
m_ranking++;
return tmp;
}
};
int main()
{
CGirl g1,g2; // 创建超女对象。
int ii=5 , jj=5;
int xx = ++(++(++ii)); cout << "xx=" << xx << ",ii=" << ii << endl;
int yy = jj++; cout << "yy=" << yy << ",jj=" << jj << endl;
CGirl g3 = ++(++(++g1)); cout << "g3.m_ranking=" << g3.m_ranking << ",g1.m_ranking=" << g1.m_ranking << endl;
CGirl g4 = g2++; cout << "g4.m_ranking=" << g4.m_ranking << ",g2.m_ranking=" << g2.m_ranking << endl;
// g2.show();
}
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)如果自动类型转换有二义性,编译将报错。
将构造函数用作自动类型转换函数似乎是一项不错的特性,但有时候会导致意外的类型转换。explicit关键字用于关闭这种自动特性,但仍允许显式转换。
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成员可以在派生类中访问,而基类中的 private成员不能在派生类中访问。
继承方式有三种:public(公有的)、protected(受保护的)和private(私有的)。它是可选的,如果不写,那么默认为private。不同的继承方式决定了在派生类中成员函数中访问基类成员的权限。
1)基类成员在派生类中的访问权限不得高于继承方式中指定的权限。例如,当继承方式为protected时,那么基类成员在派生类中的访问权限最高也为protected,高于protected的会降级为protected,但低于protected不会升级。再如,当继承方式为public时,那么基类成员在派生类中的访问权限将保持不变。
也就是说,继承方式中的public、protected、private是用来指明基类成员在派生类中的最高访问权限的。
2) 不管继承方式如何,基类中的private成员在派生类中始终不能使用(不能在派生类的成员函数中访问或调用)。
3) 如果希望基类的成员能够被派生类继承并且毫无障碍地使用,那么这些成员只能声明为public 或protected;只有那些不希望在派生类中使用的成员才声明为private。
4) 如果希望基类的成员既不向外暴露(不能通过对象访问),还能在派生类中使用,那么只能声明为 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);
}
11万+

被折叠的 条评论
为什么被折叠?



