从这篇文章开始就是C++编程的面向对象部分(核心思想),这部分是在实际应用中的重点,需要多动手,多考虑。
面向对象编程基于四个方面:封装、抽象、继承、多态
目录
类与对象
101、从结构体到类
102、类的访问权限
103、简单使用类
104、构造函数和析构函数
105、拷贝构造函数
浅拷贝和深拷贝
106、初始化列表(极其重要)
107、const修饰成员函数
108、this指针
109、静态成员
110、简单对象模型
111、友元
运算符重载
112、运算符重载基础
113、重载关系运算符
114、重载左移运算符
115、重载下标运算符
116、重载赋值运算符
动态分配内存malloc函数和释放内存free函数
117、重载new&delete运算符
118、重载括号运算符
119、重载一元运算符
C++类型转换
121、自动类型转换
122、转换函数
类继承
123、继承的基本概念
124、继承方式
125、继承的对象模型
126、如何构造基类
127、名字遮蔽与类作用域
128、继承的特殊关系
129、多继承与虚继承(简单了解即可)
类多态
131、多态的基本概念
133、多态的对象模型
134、如何析构派生类
135、纯虚函数和抽象类
136、运行阶段类型识别dynamic_cast
137、typeid运算符和type_info类
函数模版
140、自动推导类型auto
141、函数模板的基本概念
142、函数模板的注意事项
144、函数模板的具体化
145、函数模板分文件编写
146、函数模板高级
结构体和类是一个思想,只不过结构体只能一次初始化,无法灵活赋值
对面向对象编程来说,一切都是对象,对象用类来描述。
类把对象的数据和操作数据的方法作为一个整体考虑。
定义类的语法:以分号结尾
class 类名
{
public:
成员一的数据类型 成员名一;
成员二的数据类型 成员名二;
成员三的数据类型 成员名三;
......
成员n的数据类型 成员名n;
};
注意:
1 类的成员可以是变量,也可以是函数。
2 类的成员变量也叫属性。
3 类的成员函数也叫方法/行为,类的成员函数可以定义在类的外面。
4 当函数在外面定义时,在类的内部只需要填写函数声明,在外面调用时加上类名::函数。
5 用类定义一个类的变量叫创建(或实例化)一个对象。 类名 创建的对象名
6 对象的成员变量和成员函数的作用域和生命周期与对象的作用域和生命周期相同。
class tools //创建一个名为tools的类
{
public:
int num;
string name;
void setvalue(int num1, string name1); //函数声明
};
void tools::setvalue(int num1, string name1) //在函数前面加上(类名:: )表明这个函数属于类的方法
{
num = num1;
name = name1;
cout << num << name;
}
int main()
{
tools banshou; //实例化一个对象
banshou.setvalue(1, "banshou1");
}
函数(方法)在实现的时候需要将形参的值赋给实际的值,如果不需要修改参数的值,记得添加const约束。
结构体函数可以直接在函数中访问成员变量,不需要再 结构体名.成员变量
当类(结构体)函数内部在访问成员变量时可以直接访问时,会出现形参和实参的命名相同的情况,目前先将形参的值修改,后续再提供其他的方法。在内部函数中会自动传入定义的类,不需要再传递引用
class tools //创建一个名为tools的类
{
public:
int num;
string name;
void setvalue(int num1, string name1)
{
num = num1;
name = name1;
}
};
成员函数在main函数中使用时,需要加上结构体变量名。
在main函数中使用类中定义的函数(方法),创建的对象名.函数名(形参)
在main函数中使用类中成员的语法,创建的对象名.定义的成员
类的成员有三种访问权限:public、private和protected,分别表示公有的、私有的和受保护的。private用于继承
在类的内部(类的成员函数中),无论成员被声明为 public还是private,都可以访问。
在类的外部(定义类的代码之外),只能访问public成员,不能访问 private、protected成员。当成员函数公有,成员对象私有时,仍可编译,因为这属于内部访问
在下方的示例中setvalue属于public,所以在main函数中可以访问,尽管name属于private,但是通过函数的访问属于内部访问,无影响。
class tools //创建一个名为tools的类
{
public:
int num;
private:
string name;
public:
void setvalue(int num1, string name1); //函数声明
};
void tools::setvalue(int num1, string name1) //在函数前面加上(类名:: )表明这个函数属于类的方法
{
num = num1;
name = name1;
cout << num << name;
}
在一个类体的定义中,private 和 public 可以出现多次。public到private之间属于公开的,从private到下一个访问权限关键字之前都是私有的。
结构体的成员缺省(没有设置权限时)为public,类的成员缺省为private。如果类的权限不设置,则所有的成员都是私有的无法访问。(结构体和类的区别)
private的意义在于隐藏类的数据和实现,把需要向外暴露的成员声明为public。
编程思想和方法的改变,披着C++外衣的C程序员。
1)类的成员函数可以直接访问该类其它的成员函数(可以递归)。
一个成员函数中使用另一个成员函数。
2)类的成员函数可以重载,可以使用默认参数。
3)类指针的用法与结构体指针用法相同。
4)类的成员可以是任意数据类型(类中枚举)。
类中枚举很常用,在类中使用枚举,作用域是类不是整个程序,不用担心定义太多常量
enum 枚举名 { 枚举量1 , 枚举量2 , 枚举量3, ......, 枚举量n }; 枚举类的值为整数,但是在定义时是字符串
5)可以为类的成员指定缺省值(C++11标准)。
6)类可以创建对象数组,就像结构体数组一样。
7)对象可以作为实参传递给函数,一般传引用。(常用标准做法)
8)可以用new动态创建对象,用delete释放对象。
9)在类的外部,一般不直接访问(读和写)对象的成员,而是用成员函数。数据隐藏是面向对象编程的思想之一。当要访问对象的成员时,在类内部编写函数实现。
10)对象一般不用memset()清空成员变量,可以写一个专用于清空成员变量的成员函数。
因为一般使用new来对类申请一块内存,对结构体使用sizeof无意义。同结构体原因
11)对类和对象用sizeof运算意义不大,一般不用。
12)用结构体描述纯粹的数据(只有C++内置的数据类型,没有函数也没有类),用类描述对象。 结构体--纯数据 类--对象
13)在类的声明中定义的函数都将自动成为内联函数;在类的声明之外定义的函数如果使用了inline限定符,也是内联函数。
14)为了区分类的成员变量和成员函数的形参,把成员变量名加m_前缀或_后缀,如m_name或name_。(避免了形参实参同名)
15)类的分文件编写。
构造函数:在创建对象时,自动的进行初始化工作。
构造函数是一种特殊的函数,它与类同名,不返回任何值。
析构函数:在销毁对象前,自动的完成清理工作。释放资源
1)构造函数
语法:类名(){......}
访问权限必须是public。
函数名必须与类名相同。
没有返回值,不写void。
可以有参数,可以重载,可以有默认参数。
创建对象时只会自动调用一次,不能手工调用(即不可以在外部调用构造函数)。
重载即实现多个函数完成相同的功能,命名相同
加粗的文字在实现构造函数时必须满足。标黄部分是构造函数和析构函数的区别
2)析构函数
语法:~类名(){......}
访问权限必须是public。
函数名必须在类名前加~。
没有返回值,也不写void。
没有参数,不能重载。
销毁对象前只会自动调用一次,但是可以手工调用。
C++中string是std的一个类,std::string,因为string是类所以编译器会自动添加构造函数和析构函数,当这些动态分配的内存在使用时可能出现问题。
注意:
1) 如果没有提供构造/析构函数,编译器将提供空实现的构造/析构函数。
2) 如果提供了构造/析构函数,编译器将不提供空实现的构造/析构函数。
3)创建对象的时候,如果重载了构造函数,编译器根据实参匹配相应的构造函数。没有参数的构造函数(或者参数全是默认参数)也叫默认构造函数。只要不提供参数即可
4) 创建对象的时候不要在对象名后面加空的圆括号,编译器误认为是声明函数。(如果没有构造函数、构造函数没有参数、构造函数的参数都有默认参数),如果在创建的对象后面加上(),编译器会认为是一个函数 类名 对象名()≈ int func()
正确创建对象的语法 类名 创建的对象名
5) 在构造函数名后面加括号和参数不是调用构造函数,是创建匿名对象。
匿名对象没有名字,创建后马上被销毁
6) 接受一个参数的构造函数允许使用赋值语法将对象初始化为一个值(可能会导致问题,不推荐)。CGirl girl =10;
7) 以下两块代码有本质的区别:
CGirl girl = CGirl("西施"20); // 显式创建对象。
CGirl girl; // 创建对象。
girl = CGirl("西施"20); // 创建匿名对象,然后给现有的对象赋值。
第一个是创建了一个对象,第二个是创建了两个对象,先创建一个对象,之后又创建了一个临时对象,将临时对象的成员赋值给第一个对象。
8) 用new/delete创建/销毁对象时,也会调用构造/析构函数。
9) 不建议在构造/析构函数中写太多的代码(构造函数就用于初始化,析构函数就用于清理),其他的任务可以创建调用成员函数。
10)除了初始化,不建议让构造函数做太多工作(一般支持只能成功不会失败的功能)。
11)C++11支持使用统一初始化列表。
CGirl girl = {"西施"20};
CGirl girl {"西施"20};
CGirl* girl = new CGirl{ "西施"20 };
12)如果类的成员也是类,创建对象的时候,先构造成员类;销毁对象的时候,先析构成员类。
用一个已存在的对象创建新的对象,不会调用(普通)构造函数,而是调用拷贝构造函数。
如果类中没有定义拷贝构造函数,编译器将提供一个拷贝构造函数,它的功能是把已存在对象的成员变量赋值给新对象的成员变量。(代码只填写初始化和拷贝相关的,其他的代码不放在这部分)
用一个已存在的对象创建新的对象语法:两种
类名 新对象名(已存在的对象名);
类名 新对象名=已存在的对象名;
拷贝构造函数的语法:
类名(const 类名& 对象名){......}
不传引用,相当于函数的值传递,会出现浅拷贝的情况 ,const不能省略,源对象引用必须为const
()内的参数只有一个,是类本身的引用,形参的对象名无所谓。在引用前需要添加const约束。 参数只有类本身引用的拷贝构造函数也被称为默认拷贝构造函数。
注意:
访问权限必须是public。
函数名必须与类名相同。
没有返回值,不写void。
如果类中定义了拷贝构造函数,编译器将不提供默认的拷贝构造函数。(拷贝构造函数不是空实现)
以值传递的方式调用函数时,如果实参为对象,会调用拷贝构造函数。
函数以值的方式返回对象时,可能会调用拷贝构造函数(VS会调用,Linux不会,g++编译器做了优化)。
class CGirl // 超女类CGirl。 { public: string m_name; // 姓名属性。 int m_age; // 年龄属性。 // 没有参数的普通构造函数。 CGirl() { m_name.clear(); m_age = 0; cout << "调用了CGirl()构造函数。\n"; } // 没有重载的拷贝构造函数(默认拷贝构造函数)。 CGirl(const CGirl& g) { m_name = "漂亮的" + g.m_name; m_age = g.m_age - 1; cout << "调用了CGirl(const CGirl &gg)拷贝构造函数。\n"; } // 析构函数。 ~CGirl() { cout << "调用了~CGirl()\n"; } // 超女自我介绍的方法,显示姓名和年龄。 void show() { cout << "姓名:" << m_name << ",年龄:" << m_age << endl; } }; void func(CGirl g) { g.show(); } int main() { CGirl g1; //创建对象g1调用一次构造函数 g1.m_name = "西施"; g1.m_age = 23; func(g1); //生成形参对象g调用拷贝构造函数 //运行结束销毁这两个对象用到两次析构函数 }
拷贝构造函数可以重载,可以有默认参数。
类名(......,const 类名& 对象名,......){......}
在重载时,参数必须有类本身的常引用,如果没有常引用,将会成为普通的构造函数。
如果类中重载了拷贝构造函数却没有定义默认的拷贝构造函数,编译器也会提供默认的拷贝构造函数。
当在类中有一个函数是传入类对象时(相当于是按值传递),在main函数使用时在使用拷贝构造函数,有指针操作很危险。传入类对象的指针复制到形参对象的指针,但是二者指向一块内存区域。函数使用完成后,形参类对象被析构函数自动释放,传入对象的指针指向无效内存区域。
编译器提供的拷贝构造函数是浅拷贝,
浅拷贝:在复制的时候只复制指针,并没有复制指针指向的内存区域。指针指向同一块内存,当其中一个对象的指针修改内存的内容时,另一个也会随之发生改变。
深拷贝:如果指针A指向了一块内存,就申请一块同样大小的内存,让指针B指向这块内存,再将指针A指向内存的内容复制给指针B指向的内存,之后各自操作自己的内存和指针。
深拷贝的两个步骤:1 分配内存(new) 2 拷贝数据
浅拷贝的潜在风险:通常将释放内存的代码放入析构函数中,如果对象A或者对象B有一个销毁了调用了析构函数,则另一个对象的指针就成了野指针;修改一个指针的解引用,另一个指针解引用的值也会被修改。
构造函数的执行可以分成两个阶段:初始化阶段和计算阶段(初始化阶段先于计算阶段)。
初始化阶段:全部的成员都会在初始化阶段初始化。
计算阶段:一般是指用于执行构造函数体内的赋值操作。
构造函数除了参数列表和函数体之外,还可以有初始化列表。
初始化列表的语法:需要传入参数才可以使用初始化列表
类名(形参列表):成员一(值一), 成员二(值二),..., 成员n(值n)
{......}
在形参列表的括号后面加冒号,在用成员和()赋初始值。
注意:
1)如果成员已经在初始化列表中,则不应该在构造函数中再次赋值。在函数体中再次赋值将覆盖初始化列表的值。
2)初始化列表的括号中可以是具体的值,也可以是构造函数的形参名,还可以是表达式。
3)初始化列表与赋值有本质的区别,如成员是类(类中还有类),使用初始化列表调用的是成员类的拷贝构造函数,而赋值则是先创建成员类的对象(将调用成员类的普通构造函数),然后再赋值。
4)如果成员是类,初始化列表对性能略有提升。
5)如果成员是常量(在变量前加上const)和引用,必须使用初始列表,因为常量和引用只能在定义的时候初始化。
6)如果成员是没有默认构造函数的类,则必须使用初始化列表。
7)拷贝构造函数也可以有初始化列表。
8)类的成员变量可以不出现在初始化列表中,编译器使用默认的初始化方法。
9)构造函数的形参先于成员变量初始化。
注意3)的两个例子:
赋值操作
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(CBoy& bb) // 默认拷贝构造函数。 {m_xm = bb.m_xm; cout << "调用了CBoy(const CBoy &bb)拷贝构造函数。\n";} }; class CGirl // 超女类CGirl。 { public: string m_name; // 姓名属性。 int m_age; // 年龄属性。 CBoy m_boy; // 男朋友的信息。 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"; } void show() { cout << "姓名:" << m_name << ",年龄:" << m_age << ",男朋友:" << m_boy.m_xm << endl; } }; int main() { CBoy boy("子都"); CGirl g1("冰冰", 18, boy); g1.show(); }
第一行日志是main函数中创建boy对象显示的;
第二行日志是创建对象g1的时候,三个参数的普通构造函数显示的,因为实参boy是对象,所以构造函数创建形参对象boy的时候,会调用Cboy类的拷贝构造函数,所以显示第二行日志。并没有直接执行函数体的代码,而是返回去创建boy对象,并执行对应日志。
第三行日志是超女类初始化m_boy的时候显示的日志。
之后对boy类创建完成,最后执行三个参数的普通构造函数显示第四行日志。
用类创建对象的时候,先初始化构造函数的形参对象,再初始化类的成员
如果在三个参数的普通构造函数对boy类传引用 CGirl(string name, int age,CBoy& boy),运行后将没有第二行日志,超女的男朋友类是先构造,然后在构造函数中赋值(两个步骤)。
初始化列表
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(CBoy& bb) // 默认拷贝构造函数。 {m_xm = bb.m_xm; cout << "调用了CBoy(const CBoy &bb)拷贝构造函数。\n";} }; class CGirl // 超女类CGirl。 { public: string m_name; // 姓名属性。 int m_age; // 年龄属性。 CBoy m_boy; // 男朋友的信息。 //三个参数的普通构造函数。初始化列表 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(); }
第二行日志调用的是boy类的拷贝构造函数(即超女类的男朋友m_boy初始化的时候,如果不采用初始化列表,调用的是普通构造函数;如果采用了初始化列表,调用的是拷贝构造函数),如果没有初始化列表,对象的初始化和赋初始值是两个操作;采用初始化列表,对象的初始化和赋初始值是一个操作。
在类的成员函数后面加const关键字,表示在成员函数中保证不会修改调用对象的成员变量。
注意:
1)mutable可以突破const的限制,被mutable修饰的成员变量,将永远处于可变的状态,在const修饰的函数中,mutable成员也可以被修改。
2)非const成员函数可以调用const成员函数和非const成员函数。
3)const成员函数不能调用非const成员函数。
4)非const对象可以调用const修饰的成员函数和非const修饰的成员函数。
5)const对象只能调用const修饰的成员函数,不能调用非cosnt修饰的成员函数。
总结:非const可以调用所有的,但是const只能调用带有const的。
这里出现了令人纠结的三个问题:
1、为什么要保护类的成员变量不被修改?
2、为什么用const保护了成员变量,还要再定义一个mutable关键字来突破const的封锁线?
3、到底有没有必要使用const和mutable这两个关键字?
保护类的成员变量不在成员函数中被修改,是为了保证模型的逻辑正确,通过用const关键字来避免在函数中错误的修改了类对象的状态。并且在所有使用该成员函数的地方都可以更准确的预测到使用该成员函数的带来的影响。而mutable则是为了能突破const的封锁线,让类的一些次要的或者是辅助性的成员变量随时可以被更改。没有使用const和mutable关键字当然没有错,const和mutable 关键字只是给了建模工具更多的设计约束和设计灵活性,而且程序员也可以把更多的逻辑检查问题交给编译器和建模工具去做,从而减轻程序员的负担。
如果类的成员函数中涉及多个对象,在这种情况下需要使用this指针。
类对于对象的标准做法是传引用。
this指针存放了对象的地址,它被作为隐藏参数传递给了成员函数,指向调用成员函数的对象(调用者对象)。谁调用成员函数,this指针就指向谁,类的成员函数返回自己的指针或者引用都用this指针。
每个成员函数(包括构造函数和析构函数)都有一个this指针,可以用它访问调用者对象的成员。(可以解决成员变量名与函数形参名相同的问题)
*this可以表示对象。
如果在成员函数的括号后面使用const,那么将不能通过this指针修改成员变量。
类的静态成员包括静态成员变量和静态成员函数。
用静态成员可以变量实现多个对象之间的数据共享,比全局变量更安全性。
静态成员也必须放到全局区域,否则将不能正常使用
用 static 关键字把类的成员变量声明为静态,表示它在程序中(不仅是对象)是共享的。
静态成员变量不会在创建对象的时候初始化,必须在程序的全局区用代码清晰的初始化(用范围解析运算符 ::)
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)
g1.showname(); g1.showage();
CGirl::showage();
}
静态成员使用类名加范围解析运算符 :: 就可以访问,不需要创建对象也能直接访问。
如果把类的成员声明为静态的,就可以把它与类的对象独立开来(静态成员声明在类中,但是是全局的,是整个程序的,不属于对象)。
静态成员变量在程序中只有一份(生命周期与程序运行期相同,存放在静态存储区的),不论是否创建了类的对象,也不论创建了多少个类的对象。
在静态成员函数中,只能访问静态成员,不能访问非静态成员,非静态函数也不能访问。
静态成员函数中没有this指针(静态成员函数不属于对象,所以没有指针)。
在非静态成员函数中,可以访问静态成员。(在类外面访问加::,但是在类内部访问直接用变量名即可)
私有(private)静态成员在类外无法访问。(与全局变量的区别)
const静态成员变量可以在定义类的时候初始化。
在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字节的占位成员。
如果要访问类的私有成员变量,调用类的公有成员函数是唯一的办法,而类的私有成员函数则无法访问。
友元提供了另一访问类的私有成员的方案。友元有三种:
友元全局函数。
友元类。
友元成员函数。
1)友元全局函数(简单)
在友元全局函数中,可以访问另一个类的所有成员。通常是使用自定义的全局函数。
在想要访问的类内部的最上方添加friend 函数声明。
2)友元类(简单)
在友元类所有成员函数中,都可以访问另一个类的所有成员。
友元类的注意事项:
友元关系不能被继承。
友元关系是单向的,不具备交换性。
若类B是类A的友元,类A不一定是类B的友元。B是类A的友元,类C是B的友元,类C不一定是类A的友元,要看类中是否有相应的声明。
3)友元成员函数(难)
在友元成员函数中,可以访问另一个类的所有成员。
友元成员函数四个步骤:A类中有私有成员,B类中的函数func用到了A类的私有成员
1 将被访问的类A的声明前置
2 将访问的类B的定义放在被访问的类A之前
3 将访问的类B的成员函数体func从类的定义中拿出来(别忘记在函数体前面加上类名::,因为放到了全局区域),放在被访问的类A后面
4 将访问的类B的成员函数func声明为被访问的类A的友元函数(在被访问的类A中也需要加上类名::,表明是B类的函数)
class A; // 把被访问的类的声明前置 class B // 访问的类放到被访问的类前面 { public: void func1(const A& g); void func2(const A& g); }; class A // 被访问的类A。 { friend void B::func1(const A& g); //将访问的类B声明为A类的友元 friend void B::func2(const A& g); public: string m_name; // 姓名。 // 默认构造函数。 A() { m_name = "西施"; m_dz = 87; } // 显示姓名的成员函数。 void showname() { cout << "姓名:" << m_name << endl; } private: int m_dz; // 地址。 // 显示地址的成员函数。 void showxw() const { cout << "地址:" << m_dz << endl; } }; //将函数体拿出来放到被访问的类后面 void B::func1(const A& g) { cout << "func1()我女朋友的地址是:" << g.m_dz << endl; } void B::func2(const A& g) { cout << "func2()我女朋友的姓名是:" << g.m_name << endl; } int main() { A g; B b; b.func2(g); b.func1(g); }
C++将运算符重载扩展到自定义的数据类型,它可以让对象操作更美观。两个好处:1 代码书写更简单;2 更符合人类书写习惯
例如字符串string用加号(+)拼接、cout用两个左尖括号(<<)输出。
运算符重载函数的语法:返回值 operator运算符(参数列表);
运算符需要多少个参数,传入的参数列表就对应多少参数。在使用时也是对应关系(代码演示)
运算符重载函数的返回值类型要与运算符本身的含义一致。
非成员函数版本的重载运算符函数:形参个数与运算符的操作数个数相同;
成员函数版本的重载运算符函数:形参个数比运算符的操作数个数少一个,其中的一个操作数隐式传递了调用对象。
如果同时重载了非成员函数和成员函数版本,会出现二义性。只能存在一个重载函数版本。
注意:
1)返回自定义数据类型的引用可以让多个运算符表达式串联起来,内部其实是函数的多次调用。(不要返回局部变量的引用)
2)重载函数参数列表中的顺序决定了操作数的位置。
3)重载函数的参数列表中至少有一个是用户自定义的类型,防止程序员为内置数据类型重载运算符。既然要重载就证明已有的用不了,已有的可以使用不需要重载
4)如果运算符重载既可以是成员函数也可以是全局函数,应该优先考虑成员函数,这样更符合运算符重载的初衷。
5)重载函数不能违背运算符原来的含义和优先级。
6)不能创建新的运算符。
7)以下运算符只能通过成员函数进行重载
= 赋值运算符 ()函数调用运算符 [] 下标运算符 ->通过指针访问类成员运算符
加法运算符重载示例:
如果想在类中实现对某个成员加操作,不重载会导致报错。
class CGirl { private: int score; public: string name; CGirl() :name("西施"), score(30) {}; void show() { cout << name << "得分:" << score; }; }; int main() { //如果想实现加分操作 CGirl g; g + 10; g.show(); }
g+10那部分报错
使用友元函数
class CGirl { friend void addscore(CGirl& g, int add); private: int score; public: string name; CGirl() :name("西施"), score(30) {}; void show() { cout << name << "得分:" << score; }; }; void addscore (CGirl& g, int add) { g.score = g.score + add; } int main() { CGirl g; addscore(g, 30); //g + 10; g.show(); }
将友元函数转化为运算符重载来尝试(把函数名换为operator +),对应参数位置,第一个为对象,第二个是传入加的分数。
class CGirl { friend void operator+(CGirl& g, int add); private: int score; public: string name; CGirl() :name("西施"), score(30) {}; void show() { cout << name << "得分:" << score; }; }; void operator+ (CGirl& g, int add) { g.score = g.score + add; } int main() { CGirl g; //addscore(g, 30); g + 10; g.show(); }
返回值类型改为对象的引用,这样结果就可以传递给对象
class CGirl { friend CGirl& operator+(CGirl& g, int add); private: int score; public: string name; CGirl() :name("西施"), score(30) {}; void show() { cout << name << "得分:" << score; }; }; CGirl& operator+ (CGirl& g, int add) { g.score = g.score + add; return g; } int main() { CGirl g; //g = operator+(operator+(operator+(g, 10),20),30) //下方代码的实际操作过程,多次调用 g = g + 10 + 20 + 30; g.show(); }
下面的代码时重载函数成员函数版本 (不用再传引用,在内部默认传引用,返回值改为*this),然而将对象的位置进行变换也会报错。
class CGirl { private: int score; public: string name; CGirl() :name("西施"), score(30) {}; void show() { cout << name << "得分:" << score; }; CGirl& operator+ ( int add) { score = score + add; return *this; } }; int main() { CGirl g; g = 10 + 20 + 30 + g; g.show(); }
因为在使用时跟普通函数一样,传入的参数要对应,原函数:CGirl& operator+(CGirl& g, int add);使用时位置没对应,所以应该再重载个传入对象在右边的函数。用非函数成员版本,这样方便对传入参数位置变换。多写了一个传入两个对象的版本,因为两边都传递对象就相当于返回的对象+对象。
class CGirl { friend CGirl& operator+(CGirl& g, int add); friend CGirl& operator+ (int add, CGirl& g); friend CGirl& operator+ (CGirl& g1, CGirl& g2); private: int score; public: string name; CGirl() :name("西施"), score(30) {}; void show() { cout << name << "得分:" << score; }; }; CGirl& operator+ (CGirl& g, int add) { g.score = g.score + add; return g; } CGirl& operator+ (int add,CGirl& g) { g.score = g.score + add; return g; } CGirl& operator+ (CGirl& g1, CGirl& g2) { g1.score = g1.score + g2.score; return g1; } int main() { CGirl g; //g = operator+(operator+(operator+(g, 10),20),30) //下方代码的实际操作过程,多次调用 g = g + 10 ; g.show(); g = 20 + g; g.show(); //g = (g+10)+ g,两个对象相加 g = g + 10 + g; g.show(); }
重载关系运算符(==、!=、>、>=、<、<=)用于比较两个自定义数据类型的大小。
可以使用非成员函数和成员函数两种版本,建议采用成员函数版本。
class CGirl
{
private:
int m_score;
int m_score1;
public:
CGirl(int score, int score1) { m_score = score; m_score1 = score1; }
bool operator ==(CGirl& g1)
{
if ((m_score + m_score1) == (g1.m_score + g1.m_score1)){return true;}
else{return false;}
}
};
int main()
{
CGirl g(21, 29);
CGirl g1(22, 29);
if (g == g1){cout << "两个人分数相等" << endl;}
else{cout << "no" << endl;}
}
重载左移运算符(<<)用于输出自定义对象的成员变量,在实际开发中很有价值(调试和日志)。
只能使用非成员函数版本。在类外实现,声明为类的友元。
如果要输出对象的私有成员,可以配合友元一起使用。
class CGirl
{
friend ostream& operator<<(ostream& cout, CGirl& g);
private:
int m_score;
int m_score1;
public:
CGirl(int score, int score1) { m_score = score; m_score1 = score1; }
};
ostream& operator<<(ostream& shuchu, CGirl& g)
{
shuchu << "第一个参数:" << g.m_score << "第二个参数:" << g.m_score1;
return shuchu;
}
int main()
{
CGirl g(21, 29);
cout << g << endl;
}
如果对象中有数组,重载下标运算符[],操作对象中的数组将像操作普通数组一样方便。
下标运算符必须以成员函数的形式进行重载。
下标运算符重载函数的语法:
返回值类型 &perator[](参数);
或者:
const 返回值类型 &operator[](参数) const;
使用第一种声明方式,[]不仅可以访问数组元素,还可以修改数组元素。
使用第二种声明方式,[]只能访问而不能修改数组元素。
在实际开发中,我们应该同时提供以上两种形式,这样做是为了适应const对象,因为通过const 对象只能调用const成员函数,如果不提供第二种形式,那么将无法访问const对象的任何数组元素。
在重载函数中,可以对下标做合法性检查,防止数组越界。
class CGirl // 超女类CGirl。
{
private:
int m_scores[3]; // 3科分数
public:
CGirl() {m_scores[0] = 10; m_scores[1] = 20; m_scores[2] = 30; }
void show() { cout << m_scores[0] << "、" << m_scores[1] << "、" << m_scores[2] << endl; }
int& operator[](int ii)
{return m_scores[ii];}
const int& operator[](int ii) const
{return m_scores[ii];}
};
int main()
{
CGirl g; // 创建超女对象。
g[1] = 50;
cout << "第一科:" << g[1] << endl;
g.show();
const CGirl g1 = g;
cout << "第一科:" << g1[1] << endl;
}
C++编译器可能会给类添加四个函数:
默认构造函数,空实现。
默认析构函数,空实现。
默认拷贝构造函数,对成员变量进行浅拷贝。
默认赋值函数, 对成员变量进行浅拷贝。
对象的赋值运算是用一个已经存在的对象,给另一个已经存在的对象赋值。
如果类的定义中没有重载赋值函数,编译器就会提供一个默认赋值函数。
如果类中重载了赋值函数,编译器将不提供默认赋值函数。
重载赋值函数的语法:类名 & operator=(const 类名 & 源对象);
注意:
编译器提供的默认赋值函数,是浅拷贝。
如果对象中不存在堆区内存空间,默认赋值函数可以满足需求,否则需要深拷贝。
赋值运算和拷贝构造不同:拷贝构造是指原来的对象不存在,用已存在的对象进行构造;赋值运算是指已经存在了两个对象,把其中一个对象的成员变量的值赋给另一个对象的成员变量。
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 <
malloc
malloc是动态分配内存函数,用于申请一块指定大小的内存块区域以void*的类型返回分配到的内存区域地址。
malloc函数原型
extern void *malloc(unsigned int num_bytes);
申请num_bytes字节长度的内存区域,使用时导入头文件#include
分配成功将返回指向分配内存地址的指针,否则将返回空指针。
注意
因为malloc函数返回的是void*指针类型,在使用时必须强制转化为自己想要使用的类型。
使用malloc函数在申请内存空间后,使用完成后必须释放,否则会造成内存泄漏。
在malloc函数申请的空间中不要进行指针的移动,移动后可能导致释放的空间与申请的空间大小不一致。
int *p = NULL;
int n = 10;
p = (int *)malloc(sizeof(int)*n);
申请的内存空间必须在申请前就自己明确申请的大小,申请空间后为了避免申请的空间重复使用,系统对使用的空间做标记,表面该空间正在被使用
free
free函数用于释放malloc函数给指针变量分配的内存区域。
在使用free函数后,最好将指针变量指向空指针,防止出现误操作。
int main()
{
int *p = (int *)malloc(sizeof(int));
*p = 100;
free(p);
p = NULL;
return 0;
}
重载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*。按照malloc的过程实现,malloc函数返回值就是void*,传入的参数是申请的内存空间大小。
2)重载内存释放函数的语法:void operator delete(void* ptr)
参数必须是void *(指向由operator new()分配的内存),返回值必须是void。根据free函数实现
重载的new和delete可以是全局函数,也可以是类的成员函数。函数实现在全局域就是全局函数,在类内部就是类的成员函数。
为一个类重载new和delete时,尽管不必显式地使用static,但实际上仍在创建static成员函数。
编译器看到使用new创建自定义的类的对象时,它选择成员版本的operator new()而不是全局版本的new()。
new[]和delete[]也可以重载。
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\n";
if (ptr == 0) return; // 对空指针delete是安全的。
free(ptr); // 释放内存。
}
class CGirl // 超女类CGirl。
{
public:
int m_bh; // 编号。
int m_cg; // 成绩。
CGirl(int bh, int xw) { m_bh = bh, m_cg = 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_cg << endl;
delete p2;
}
内存池(重载new和delete在实际开发的应用)
预先分配一大块内存空间
提升分配和归还的速度
减少内存碎片
如果一个类有8个字节,想系统申请连续18字节的内存空间,如上图,除了本身申请的8*2=16个字节外,还有两个字节是标志位,用于表明内存是否被使用。标志位的取值只有0和1,0表示空闲,1表示被占用。
在使用内存池的时候,先判断标志位是否被占用,如果没被占用就返回标志位下一个字节的地址。
内存池一般只有成员函数类型的new和delete,并且一般有一个指针指向内存池起始地址。
重载的new和delete哪怕不写static关键字,也会默认为类的静态成员函数,在类的静态成员函数只能访问类的静态成员变量,所以需要在指针和函数前都加上static关键字。内存池的指针是静态成员变量需要在main函数外面初始化,char* CGirl::m_pool = 0;
内存池用完的三种处理:1 扩展内存池 2 直接向系统申请内存 3 返回空地址。根据需求选择。
class CGirl // 超女类CGirl。
{
public:
int m_bh; // 编号。
int m_cg; // 成绩。
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_cg = 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_cg << endl;
CGirl* p2 = new CGirl(4, 7); // 将使用内存池的第二个位置。
cout << "p2的地址是:" << p2 << ",编号:" << p2->m_bh << ",成绩:" << p2->m_cg << endl;
CGirl* p3 = new CGirl(6, 9); // 将使用系统的内存。
cout << "p3的地址是:" << p3 << ",编号:" << p3->m_bh << ",成绩:" << p3->m_cg << endl;
delete p1; // 将释放内存池的第一个位置。
CGirl* p4 = new CGirl(5, 3); // 将使用内存池的第一个位置。
cout << "p4的地址是:" << p4 << ",编号:" << p4->m_bh << ",成绩:" << p4->m_cg << endl;
delete p2; // 将释放内存池的第二个位置。
delete p3; // 将释放系统的内存。
delete p4; // 将释放内存池的第一个位置。
CGirl::freepool(); // 释放内存池。
}
释放完的内存可以重新分配给其他人。
括号运算符()也可以重载,对象名可以当成函数来使用(函数对象、仿函数)。
括号运算符重载函数的语法:
返回值类型 operator()(参数列表);
注意:
括号运算符必须以成员函数的形式进行重载。
括号运算符重载函数具备普通函数全部的特征。
如果函数对象与全局函数同名,按作用域规则选择调用的函数。
函数对象的用途:
1)表面像函数,部分场景中可以代替函数,在STL中得到广泛的应用;
2)函数对象本质是类,可以用成员变量存放更多的信息;
3)函数对象有自己的数据类型;
4)可以提供继承体系。
void show(string str) // 向超女表白的函数。
{
cout << "普通函数:" << str << endl;
}
class CGirl // 超女类。
{
public:
void operator()(string str) // 向超女表白的函数。
{cout << "重载函数:" << str << endl;}
};
int main()
{
CGirl show;
::show("我是一只傻傻鸟。");
show("我是一只傻傻鸟。");
}
函数名冲突的情况下,想要使用全局函数,只需要在对象函数名前加::即可
可重载的一元运算符。
1)++ 自增 2)-- 自减 3)! 逻辑非 4)& 取地址
5)~ 二进制反码 6)* 解引用 7)+ 一元加 8) - 一元求反
一元运算符通常出现在它们所操作的对象的左边。
但是,自增运算符++和自减运算符--有前置和后置之分。
C++ 规定,重载++或--时,如果重载函数有一个int形参,编译器处理后置表达式时将调用这个重载函数。形参传入int就成为后置
++和--的后置不能嵌套使用,并且在成员函数重载的++后置和实际的++后置有区别。
成员函数版:CGirl &operator++(); // ++前置
成员函数版:CGirl operator++(int); // 后置++
非成员函数版:CGirl &operator++(CGirl &); // ++前置
非成员函数版:CGirl operator++(CGirl &,int); // 后置++
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();
}
所以需要使用一个临时对象来保存对象的当前状态,++之后返回临时对象,并且返回值不能是引用,临时对象不能引用。
修改完之后就是上面的完整代码
改完之后符合++后置的语法
对于内置类型,如果两种数据类型是兼容的,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。
构造函数只用于从某种类型到类类型的转换,如果要进行相反的转换,可以使用特殊的运算符函数-转换函数。
语法:operator 数据类型();
注意:转换函数必须是类的成员函数;不能指定返回值类型;不能有参数。
可以让编译器决定选择转换函数(隐式转换),可以像使用强制类型转换那样使用它们(显式转换)。
int ii=girl; // 隐式转换。
int ii=(int) girl; // 显式转换。
int ii=int(girl); // 显式转换。
如果隐式转换存在二义性,编译器将报错。
在C++98中,关键字explicit不能用于转换函数,但C++11消除了这种限制,可以将转换函数声明为显式的。
还有一种方法是:用一个功能相同的普通成员函数代替转换函数,普通成员函数只有被调用时才会执行。
int ii=girl.to_int();
警告:应谨慎的使用隐式转换函数。通常,最好选择仅在被显式地调用时才会执行的成员函数。
继承可以理解为一个类从另一个类获取成员变量和成员函数的过程。
语法:继承方式不注明的话默认私有
class 派生类名:[继承方式]基类名
{
派生类新增加的成员
};
class fish
{
//基类的相关描述
};
class goldfish:public fish
{
//派生类的相关描述
};
被继承的类称为基类或父类,继承的类称为派生类或子类。基类的私有成员在派生类中不可见
继承和派生是一个概念,只是站的角度不同。
派生类除了拥有基类的成员,还可以定义新的成员,以增强其功能。派生类可以使用基类公开的方法。
使用继承的场景:
1) 如果新创建的类与现有的类相似,只是多出若干成员变量或成员函数时,可以使用继承。
2) 当需要创建多个类时,如果它们拥有很多相似的成员变量或成员函数,可以将这些类共同的成员提取出来,定义为基类,然后从基类继承。
类成员的访问权限由高到低依次为:public --> protected --> private,public成员在类外可以访问,private成员只能在类的成员函数中访问。类可以在函数中访问自己的私有成员
使用protected是因为基类的相关内容希望派生类可以使用,但是又不希望在外部函数中修改基类的内容(默认参数),如果使用private在派生类无法看到。protected在派生类和友元中可以访问,但是main函数无法使用。
如果不考虑继承关系,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。
在派生类中,可以通过基类的公有成员函数间接访问基类的私有成员。
class A { // 基类
public:
int m_a = 10;
void func() { cout << m_c; }
private:
int m_c = 30;
};
class B :public A // 派生类
{
};
int main()
{
B b;
// b.m_a = 11;
b.func();
}
使用 using 关键字可以改变基类成员在派生类中的访问权限。想改成那个权限就放到哪个权限的下方,使用方法为基类名::
注意:using只能改变基类中public和protected成员的访问权限,不能改变private成员的访问权限,因为基类中的private成员在派生类中是不可见的,根本不能使用。
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_b = 21;
}
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
派生类构造函数的要点如下:
1)创建派生类对象时,程序首先调用基类构造函数,然后再调用派生类构造函数。
2)如果没以指定基类构造函数,将使用基类的默认构造函数。
3)可以用初始化列表指明要使用的基类构造函数。
4)基类构造函数负责初始化被继承的数据成员;派生类构造函数主要用于初始化新增的数据成员。
5)派生类的构造函数总是调用一个基类构造函数,包括拷贝构造函数。
类继承的一个原则:基类的成员变量由基类的构造函数初始化,派生类新增的成员变量由派生类的构造函数初始化。
疑问:由派生类一起初始化基类的成员不可以吗
不可以。1 基类的私有成员在派生类不可见,没办法初始化,(既然基类的私有成员不可见,不用它行不行,不初始化行不行,不行,不可见不代表不能用,可以使用基类的公共函数来使用它)2 抛开基类的私有成员的因素,把初始化基类成员的代码写到派生类中,当基类被多个派生类继承时,每个派生类都要初始化基类的成员,代码重复,不符合继承的理念。
如果派生类中的成员(包括成员变量和成员函数)和基类中的成员重名,通过派生类对象或者在派生类的成员函数中使用该成员时,将使用派生类新增的成员,而不是基类的。
注意:基类的成员函数和派生类的成员函数不会构成重载,如果派生类有同名函数,那么就会遮蔽基类中的所有同名函数。有点类似Python中的覆盖
类是一种作用域,每个类都有它自己的作用域,在这个作用域之内定义成员。
在类的作用域之外,普通的成员只能通过对象(可以是对象本身,也可以是对象指针或对象引用)来访问,静态成员可以通过对象访问,也可以通过类访问。
在成员名前面加类名和域解析符可以访问对象的成员。(继承关系派生类重写基类函数,但是在使用时,想使用基类的函数,实例对象.基类::方法)
如果不存在继承关系,类名和域解析符可以省略不写。
当存在继承关系时,基类的作用域嵌套在派生类的作用域中。如果成员在派生类的作用域中已经找到,就不会在基类作用域中继续查找;如果没有找到,则继续在基类作用域中查找。
如果在成员的前面加上类名和域解析符,就可以直接使用该作用域的成员。
派生类和基类之间有一些特殊关系。
1)如果继承方式是公有的,派生类对象可以使用基类成员。
2)可以把派生类对象赋值给基类对象(包括私有成员),但是,会舍弃非基类的成员。
3)基类指针可以在不进行显式转换的情况下指向派生类对象。
4)基类引用可以在不进行显式转换的情况下引用派生类对象。(引用也是指针)
注意:
1)基类指针或引用只能调用基类的方法,不能调用派生类的方法。
2)可以用派生类构造基类。
3)如果函数的形参是基类,实参可以用派生类。
4)C++要求指针和引用类型与赋给的类型匹配,这一规则对继承来说是例外。但是,这种例外只是单向的,不可以将基类对象和地址赋给派生类引用和指针(没有价值,没有讨论的必要)。
多继承的语法:
class 派生类名 : [继承方式1] 基类名1, [继承方式2] 基类名2,......
{
派生类新增加的成员
};
多继承的相关事项和单继承一样
菱形继承
虚继承可以解决菱形继承的二义性和数据冗余的问题。
有了多继承,就存在菱形继承,有了菱形继承就有虚继承,增加了复杂性。
不提倡使用多继承,只有在比较简单和不出现二义性的情况时才使用多继承,能用单一继承解决的问题就不要使用多继承。使用虚继承时只需要在继承方式后面加上关键字virtual
如果继承的层次很多、关系很复杂,程序的编写、调试和维护工作都会变得更加困难,由于这个原因,C++之后的很多面向对象的编程语言,例如 Java、C#、PHP 等,都不支持多继承。
面向对象编程的核心--多态,多态也可以称为子类型多态。
基类指针只能调用基类的成员函数,不能调用派生类的成员函数。
如果在基类的成员函数前加virtual 关键字,把它声明为虚函数,基类指针就可以调用派生类中同名的成员函数,通过派生类中同名的成员函数,就可以访问派生对象的成员变量。
有了虚函数,基类指针指向基类对象时就使用基类的成员函数和数据,指向派生类对象时就使用派生类的成员函数和数据,基类指针表现出了多种形式,这种现象称为多态。
基类引用也可以使用多态。
使用多态的方法:在基类的成员函数前加virtual 关键字,把它声明为虚函数,创建基类指针,让其指向派生类对象,用基类指针调用派生类的成员函数。基类指针指向new创建的派生类对象,之后再用指针调用函数。
注意:
1)只需要在基类的函数声明中加上virtual关键字,函数定义时不能加。
2)在派生类中重定义虚函数时,函数特征要相同。
3)当在基类中定义了虚函数时,如果派生类没有重定义该函数,那么将使用基类的虚函数。
4)在派生类中重定义了虚函数的情况下,如果想使用基类的虚函数,可以加类名和域解析符。
5)如果要在派生类中重新定义基类的函数,则将它设置为虚函数;否则,不要设置为虚函数,有两方面的好处:首先效率更高;其次,指出不要重新定义该函数。
对于在派生类中覆盖的基类方法,在声明时务必声明为虚函数
如果派生类想要覆盖基类的方法,最好在继承方式 继承的基类 后面加上override关键字
《21天学通C++》多态的相关解释
当在基类中的函数在派生类被覆写时,如果将派生类实例,作为实参传递给基类对象,则该参数在调用函数时,使用的是基类::函数。
存在的问题:将派生类对象传递给基类参数时,通过该参数调用函数时,将执行基类的函数。
class fish()
{
public:
void swim()
{
cout<<"fish swim";
}
};
class goldfish():public fish
{
public:
void swim()
{
cout<<"goldfish swim";
}
};
void usefuction(fish& inputfish)
{
input.swim
}
int main()
{
goldfish myfish;
myfish.swim();
usefuction(myfish);
return 0;
}
上方代码的输出结果为
goldfish swim
fish swim
如果在基类的函数前加上virtual关键字将会实现派生类覆写的函数。
class fish()
{
public:
virtual void swim()
{
cout<<"fish swim";
}
};
class goldfish():public fish
{
public:
void swim()
{
cout<<"goldfish swim";
}
};
void usefuction(fish& inputfish)
{
input.swim
}
int main()
{
goldfish myfish;
myfish.swim();
usefuction(myfish);
return 0;
}
输出的结果为:
goldfish swim
goldfish swim
这就是多态,将派生类对象视为基类对象,执行派生类的函数实现
类的普通成员函数的地址是静态的,在编译阶段已指定。
如果基类中有虚函数,对象的内存模型中有一个虚函数表,表中存放了基类的函数名和地址。
如果派生类中重定义了基类的虚函数,创建派生类对象时,将用派生类的函数取代虚函数表中基类的函数。
C++中的多态分为两种:静态多态与动态多态。
静态多态:也成为编译时的多态;在编译时期就已经确定要执行了的函数地址了;主要有函数重载和函数模板。
动态多态:即动态绑定,在运行时才去确定对象类型和正确选择需要调用的函数,一般用于解决基类指针或引用派生类对象调用类中重写的方法(函数)时出现的问题。
构造函数不能继承,创建派生类对象时,先执行基类构造函数,再执行派生类构造函数。
析构函数不能继承,而销毁派生类对象时,先执行派生类析构函数,再执行基类析构函数。
派生类的析构函数在执行完后,会自动执行基类的析构函数。
如果手工的调用派生类的析构函数,也会自动调用基类的析构函数。
在基类函数中最好包含虚析构函数
如果编写了下面代码
base* pbase = new derived delete pbase
只有当基类析构函数~base()声明为虚函数时,delete pbase才会调用派生类析构函数~derived()
析构派生类的要点如下:
1)析构派生类对象时,会自动调用基类的析构函数。与构造函数不同的是,在派生类的析构函数中不用显式地调用基类的析构函数,因为每个类只有一个析构函数,编译器知道如何选择,无需程序员干涉。
2)析构函数可以手工调用,如果对象中有堆内存,析构函数中以下代码是必要的:
delete ptr;
ptr=nulllptr;
3)用基类指针指向派生类对象时,delete基类指针调用的是基类的析构函数,不是派生类的,如果希望调用派生类的析构函数,就要把基类的析构函数设置为虚函数。这样可以防止new出来的派生类对象没被释放。
4)C++编译器对虚析构函数做了特别的处理。
5)对于基类,即使它不需要析构函数,也应该提供一个空虚析构函数。
6)赋值运算符函数不能继承,派生类继承的函数的特征标与基类完全相同,但赋值运算符函数的特征标随类而异,它包含了一个类型为其所属类的形参。
7)友元函数不是类成员,不能继承。
纯虚函数是一种特殊的虚函数,在某些情况下,基类中不能对虚函数给出有意义的实现,把它声明为纯虚函数。知道派生类要用到这个函数,但是具体实现不确定
纯虚函数只有函数名、参数和返回值类型,没有函数体,具体实现留给该派生类去做。
语法:virtual 返回值类型 函数名 (参数列表)=0;
纯虚函数在基类中为派生类保留一个函数的名字,以便派生类它进行重定义。如果在基类中没有保留函数名字,则无法支持多态性。
含有纯虚函数的类被称为抽象类,不能实例化对象,可以创建指针和引用。
派生类必须重定义抽象类中的纯虚函数,否则也属于抽象类。
基类中的纯虚析构函数也需要实现。
有时候,想使一个类成为抽象类,但刚好又没有任何纯虚函数,怎么办?
方法很简单:在想要成为抽象类的类中声明一个纯虚析构函数。
运行阶段类型识别(RTTI RunTime Type Identification)为程序在运行阶段确定对象的类型,只适用于包含虚函数的类。
基类指针可以指向派生类对象,如何知道基类指针指向的是哪种派生类的对象呢?(想调用派生类中的非虚函数)。
dynamic_cast运算符用指向基类的指针来生成派生类的指针,它不能回答“指针指向的是什么类的对象”的问题,但能回答“是否可以安全的将对象的地址赋给特定类的指针”的问题。
语法:派生类指针 = dynamic_cast<派生类类型 *>(基类指针);
如果转换成功,dynamic_cast返回对象的地址,如果失败,返回nullptr。
注意:
1)dynamic_cast只适用于包含虚函数的类。
2)dynamic_cast可以将派生类指针转换为基类指针,这种画蛇添足的做法没有意义。
3)dynamic_cast可以用于引用,但是,没有与空指针对应的引用值,如果转换请求不正确,会出现bad_cast异常。
typeid运算符用于获取数据类型的信息。
语法一:typeid(数据类型);
语法二:typeid(变量名或表达式);
typeid运算符返回type_info类(在头文件
type_info类的实现随编译器而异,但至少有name()成员函数,该函数返回一个字符串,通常是类名。
type_info重载了==和!=运算符,用于对类型进行比较。
注意:
1)type_info类的构造函数是private属性,也没有拷贝构造函数,所以不能直接实例化,只能由编译器在内部实例化。
2)不建议用name()成员函数返回的字符串作为判断数据类型的依据。(编译器可能会转换类型名)
3)typeid运算符可以用于多态的场景,在运行阶段识别对象的数据类型。
4)假设有表达式typeid(*ptr),当ptr是空指针时,如果ptr是多态的类型,将引发bad_typeid异常。
在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 // 包含头文件。
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);
}
如果函数的形参个数发生变化,则在main函数的函数指针声明中也需要做对应修改,但是如果使用auto自动推导,只需要在使用时修改传入参数的个数即可。
剩下3中的使用以后再演示
函数模板是通用的函数描述,使用任意类型(泛型)来描述函数。
编译的时候,编译器推导实参的数据类型,根据实参的数据类型和函数模板,生成该类型的函数定义。
生成函数定义的过程被称为实例化。实例化:用模版生成具体函数
创建交换两个变量的函数模板:T的名字可以自行修改,大部分人习惯用T或者anytype
template
T> void Swap(T &a, T &b)
{
T tmp = a;
a = b;
b = tmp;
}
如果不使用函数模版则需要多次重载,传递多个数据类型
在C++98添加关键字typename之前,C++使用关键字class来创建模板。
如果考虑向后兼容,函数模板应使用typename,而不是class。
函数模板实例化可以让编译器自动推导,也可以在调用的代码中显式的指定。
1)可以为类的成员函数创建模板,但不能是虚函数和析构函数。
class CGirl { public: template
CGirl(T a) { cout << "a=" << a << endl; } template void show(T b) { cout << "b=" << b << endl; } }; int main() { CGirl g("西施"); g.show(3); g.show("xishi"); } 2)使用函数模板时,必须明确数据类型,确保实参与函数模板能匹配上。当没有参数传入时必须在括号前明确参数类型,否则无法使用函数模版。
class CGirl { public: template
CGirl(T a) { cout << "a=" << a << endl; } template void show( ) { cout << "调用函数模版" << endl; } }; int main() { CGirl g("西施"); g.show (); } 3)使用函数模板时,推导的数据类型必须适应函数模板中的代码。
例如对类执行加法运算,尽管可以推导出是超女类,但是执行会报错
4)使用函数模板时,如果是自动类型推导,不会发生隐式类型转换,如果显式指定了函数模板的数据类型,可以发生隐式类型转换。
例如在C++中字符就是数字,但是必须显式表明int才会对字符隐式转换
template
T ADD (T a, T b) { return a + b; } int main() { int a = 10; char b = 'x'; int c = ADD (a, b); cout << c; } 5)函数模板支持多个通用数据类型的参数。生成1个通用参数,在使用时,这1个通用参数必须保持一致,不能既自动推导为int,又推导为double。生成多个则没这个困扰
template
void show (T1 a, T2 b) { cout << a << b; } int main() { int a = 10; char b = 'x'; show(a, b); return 0; } 6)函数模板支持重载,可以有非通用数据类型的参数。参数除了传递自动推到的,还可以加上自己确定的。
可以提供一个具体化的函数定义,当编译器找到与函数调用匹配的具体化定义时,将使用该定义,不再寻找模板。
具体化(特例化、特化)的语法:声明、定义
template<> void 函数模板名<数据类型>(参数列表)
template<> void 函数模板名 (参数列表)
{
// 函数体。
}
class CGirl // 超女类。
{
public:
int m_bh; // 编号。
string m_name; // 姓名。
int m_rank; // 排名。
};
template
void Swap(T& a, T& b); // 交换两个变量的值函数模板。
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
void Swap(T& a, T& b) // 交换两个变量的值函数模板。
{
T tmp = a;
a = b;
b = tmp;
cout << "调用了Swap(T& a, T& b)\n";
}
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";
}
对于给定的函数名,可以有普通函数、函数模板和具体化的函数模板,以及它们的重载版本。
编译器使用各种函数的规则:
1)具体化优先于常规模板,普通函数(优先级最高)优先于具体化和常规模板。
2)如果希望使用函数模板,可以用空模板参数强制使用函数模板。(在有普通函数的情况下想使用具体的函数模版。)
3)如果函数模板能产生更好的匹配,将优先于普通函数。例如字符的交换(字符的本质是函数)
1 函数模板只是函数的描述,没有实体,创建函数模板的代码放在头文件中。
2 函数模板的具体化有实体,编译的原理和普通函数一样,所以,声明放在头文件中,定义放在源文件中。
普通函数和具体化的函数模版在头文件声明,在源文件定义
创建函数模版的声明和定义全部内容都在头文件
1)decltype关键字(更多见)
在C++11中,decltype操作符,用于查询表达式的数据类型。
语法:decltype(expression) var;
decltype分析表达式并得到它的类型,不会计算执行表达式。函数调用也一种表达式,因此不必担心在使用decltype时执行了函数。
decltype推导规则(按步骤):
1)如果表达式是一个没有用括号括起来的标识符,则var的类型与该标识符的类型相同,包括const等限定符。
2)如果表达式是一个函数调用,则var的类型与函数的返回值类型相同(函数不能返回void,但可以返回void *)。
3)如果表达式是一个左值(能取地址)(要排除第一种情况)、或者用括号括起来的标识符,那么var的类型是expression的引用。
4)如果上面的条件都不满足,则var的类型与表达式的类型相同。
decltype的结果只有两种情况:1 和表达式的类型相同,2 是表达式类型的引用。用错了编译器会报错,报错就换种写法,多试几次就行。
如果需要多次使用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
{
// 函数体。
}
函数写之前因为传入参数不确定,所以输出的数据类型也不确定,使用decltype和auto一起实现
template
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.5);
}
3)C++14的auto关键字
C++14标准对函数返回类型推导规则做了优化,函数的返回值可以用auto,不必尾随返回类型。