C++
- 知识基础
- 流操作符
- 类 & 基础
- 初始化和重载
- 类 & 高级
- 继承和多态
- 模板 & 标准模板库STL
目录
一、初始化
在C语言的编程中我们能够发现,在定义函数中,不论我们使用怎样的数据,总是需要对定义的变量进行赋初值的操作、对定义的数据结构初始化、对调用的函数进行参数传递!
C++中我们以面向对象的角度来看,在我封装好的类之中,让我调用者来进行类的成员初始值,这是比较难以接受的,很可能我并不关心、并不了解你定义的类之中有什么样的成员,我怎么去正确的初始化呢?
- C++中支持了默认参数,在任何的函数调用中可以省略函数实参;
- 构造函数和析构函数完成定义一个类的对象时对各种成员的控制,也就是初始化!
以上,这两种机制结合函数重载能够让用户视角的类的使用更加灵活、方便!
1.函数默认参数
定义方法:
函数默认参数要在函数名出现最早的位置给出;
- 函数原型
void fun(int a=1, char b='2');
可以省略形参名,对应的是函数定义的形参名 - 函数定义
void fun(int a=1, char b='2'){函数体}
注意事项:
- 定义默认参数时,顺序为自右到左。即如果一个参数设定了缺省值时,其右边的参数都要有缺省值。
- 默认参数调用时,遵循参数调用顺序,自左到右逐个调用。
- 参数的默认值必须是常量!
2.构造函数、析构函数
特殊的成员函数!!!
构造函数
基本构造函数: 在创建对象进行实例化时,自动调用构造函数,执行其中的内容。
class Test{
public:
Test(){
内联构造函数
}
}
Test::Test(){
外联构造函数
}
- 该函数与类同名!没有返回值类型,不能写!
- 可以是内联函数、外联函数;
- 可以重载;
- 可以设置参数或者无参数!
主要是对指针类型分配空间,或者可以在定义对象的初始化进行控制!
带参构造函数:我们在对类进行实例化过程中,需要将一些数据传递给定义的对象,也就是初始化,可以通过带参构造函数将数据写入对象;
class Test{
Test(int a, int b){
带参构造函数
}
}
int main(){
Test t(1,2);//在这里初始化,实际上是调用了构造函数,传递了两个参数
retunr 0;
}
注意,如果只设置了这样一个带参的构造函数,在定义对象时必须传递实参!
默认参数的构造函数:作为一个函数,构造函数同样可以使用默认参数。
缺省构造函数:(1)类中未定义构造函数,系统自动提供一个无参的;(2)类中定义的无参构造函数;(3)定义了带参的构造函数但是每一个参数都具有默认值!
一个类只能有一个缺省构造函数
当一个类中定义了缺省构造函数时,在定义对象时是不用传递数据的。
重载构造函数:显然我们对一个复杂类对象的初始化是比较复杂的,可能输入不同类型数量的数据对同一类对象初始化,但是构造函数只有一个名字,如何分辨这样的区别来进行初始化呢?——重载!!!
通过函数重载可以实现 缺省的初始化、带参的初始化…等操作。
初始化对象数组:定义同一类的一个数组,想要对数组的每一个元素进行初始化;
Test t[2] = {1, 2};
//多个参数的构造函数,采用函数调用的形式
Test t[2] = {Test(1, '1'), Test(2, '2')};
析构函数
析构函数名字为~类名
当对象生存周期结束时自动调用析构函数,经常执行的操作是动态释放空间。
- 析构函数一定没有参数;
- 析构函数不能重载;
- 没有返回值,也没有返回值类型
class Test{
Test();//构造函数
~Test(){
析构操作
};
}
二、重载
1.函数重载
定义多个函数,它们的名字相同,但是参数的类型或者参数的个数不完全相同!
针对一些功能类似的模块,我们只改变参数类型
、参数数量
,其中的操作和最终实现的功能是大同小异的,主要是可以方便我们的函数调用。
注意,不能只通过返回值的类型来区别函数的重载!
int mul(int a){
return a*a;
}
float mul(float a){
return a*a;
}
int mul(int a, int b){
int a*b;
}
重载函数的好处体现在调用的时候,当我们设计好一个完整的类时,对调用者提供的接口中可以用接口名称简单直接的体现成员的功能,调用者只要是传入合法的参数,通过重载都能满足需求,而不需要设计很多的接口函数,让调用者去选择;
例:
对于构造函数的重载:一个缺省构造函数,允许直接初始化对象;多个带参构造函数(无默认值),允许带参初始化对象;这样就可以,灵活的初始化对象!
class Test{
Test(){
缺省构造函数
};
Test(int a){
重载
};
}
Test::Test(int a, int b){
重载
}
int main(){
Test t1;//使用缺省构造函数
Test t2(1);//使用一个参数构造函数
Test t3(1, 2);//使用两个参数构造函数
return 0;
}
2.拷贝构造函数
拷贝构造函数是特殊的对构造函数的重载,使用情景:
- 用同类的一个对象去初始化另一个对象!
- 函数的形参是对象,参数传递时调用;
- 函数返回值是对象,函数执行结束调用函数暂时拷贝对象。
int fun(Test t){
return 0;
}
int mian(){
Test a;
Test b = a;//采用了=运算符
fun(b);//形参是对象
return 0;
}
如上所示的这种初始化方法,会有什么问题呢???
按位复制,对象的各个数据成员都是按照存储空间内的内容,完全一致的复制下来,这样的操作称为浅复制。
对于指针成员的复制,如果是按位复制,会造成指针指向同一块空间,这是很危险的,多个对象指向同一块空间,它的改变将难以管理。
C++中会自动重载一个拷贝构造函数
:
class Test{
Test(Test& a){
默认情况下,按位赋值
}
}
- 该重载函数的形参是该
类的一个引用
,注意必须是引用类型(防止调用自身); - 为防止修改原对象,我们可以设置参数类型为常引用
const Test& a
; - 如果用户没有自定义,系统会自动设置一个拷贝构造函数,在使用一个对象初始化另一个对象时,自动调用拷贝构造函数,实现默认的按位复制;
int main(){
Test b(a);
return 0;
}
可以看到我们之前使用的是b=a
这样的方式复制的,但是按照构造函数的方式应该如上这样b(a)
进行操作,为什么?
默认的拷贝构造函数下b=a
等价于b(a)
,哪一种方式更好呢?
显然,使用=
操作符更加符合我们的习惯,这类似于C++中对运算符的重载!初始化语句中的b=a
能够被识别,当作b(a)
处理,这实现起来实现简单,只要是对象间初始化自动转为b(a)
就行了;
那么在跟一般的情况下呢?
int main(){
Test a, b;
a = b;//一般的赋值操作
return 0;
}
这里的赋值语句在系统看来是无法识别的。类比基本数据类型我们定义的运算符是能够正确识别如何去运算的,这是取决于基本数据类型的底层设计,整型、浮点型等的加减乘除等都有硬件的支持;但自定义的抽象类型是没有这样的支持的,如果我们想当然的使用运算符去操作定义的对象,自然是不对的!一种方式是利用成员函数专门来实现这些计算,类似于拷贝构造函数的b(a)
使用,或者运算符重载。
class Test{
public:
void add(){
实现加法功能的函数
}
}
int main(){
Test a;
a.add(2);//通过函数方式实现加法功能
return 0;
}
3.运算符重载
函数重载是通过识别参数列表来区分函数的,运算符重载是通过识别运算对象来区分执行什么样的运算的!,使得对于对象的一些运算加减乘除能够符合习惯的认知。
重载赋值运算符
class Test{
void operator=(const Test & right){
使用 right 对象
对本对象数据成员进行赋值操作
right.数据成员名//右部的数据成员
数据成员名//指示本对象的数据成员
}
}
- 使用的形式参数为常引用,表示无意修改右部的值;
- 同样能够以函数形式调用
b.operator=(a);
等价于b = a
; - 区分左右部的方式是右部作为参数必须使用成员运算符才能访问自己的成员,而左部是主体,是调用当前函数的对象,直接使用相应的成员名就行;
this指针
在以上的定义之中,我们重载赋值运算符之后没有返回值,当我们让a = b
的返回值为a
,这样更加符合我们习惯的基本操作,这就需要我们表示当前调用的对象是哪一个?this指针
指向了当前的主体:
class Test{
Test operator=(const Test & right){
赋值操作
this->数据成员//指示本对象的数据成员
return * this;//注意这是个指针昂
}
}
- this是隐含的内嵌指针,指向调用成员函数的当前对象;
- this实际上是每一个非静态成员函数的隐含参数;
- 形参与数据成员同名的情况必须使用this指针区分;
重载双目运算符
- 重载运算符最好不要改变原义;
- 重载不能改变原本要求的参数个数要求;
- 有少部分运算符不允许重载:
?:
、.
、.*
、::
、sizeof
;
class Test{
Test operator+(const Test & right){
Test ret;//存储求和结果
ret.数据成员 由 this->数据成员 和 right.数据成员 生成
return ret;
}
}
//调用
int main(){
Test a, b, c;
a = b + c;//使用了重载后的+ =
a = b.operator+(c);
return 0;
}
重载单目运算符
- 单目运算符只处理当前对象,因此运算符重载函数不需要参数;
- 我们需要区分++、–的前置和后置!!!
前置
class Test{
Test operator++(){//没有参数,认为是前置的单目++
++数据成员;
return * this;
}
}
//调用
int main(){
Test a;
++a;
a.operator++();//等价
return 0;
}
后置(使用哑元
只有类型,没有名字)
class Test{
Test operator++(int){//形参只有类型,没有名称
Test ret = * this;//初始化返回值,记录变化前的值
数据成员++;
return ret;
}
}
//调用
int main(){
Test a;
a++;
a.operator++(0);//等价,形参符合定义类型的任意值
return 0;
}
重载关系运算符
重载关系运算符实现两个对象的比较,类似于重载二元运算符,但是返回值类型为bool
(true或false)。
class Test{
bool operator>(Test & right){
根据数据成员关系判断
返回值为true or false
}
}
重载流操作符运算符
方便的实现对cin、cout对象的输出,而无需使用函数成员显式输入输出;使用到 友元函数 ,我们在下一节《类 | 高级》中做讲解!
重载类型转换运算符
对赋值运算符两端不同数据类型的隐式类型转换,对于抽象类我们同样可以通过重载实现!
- 类型转换重载是没有返回值的;
- 没有形参,只是对调用对象的操作;
- 最好只定义一个与算术类型有关的重载,避免二义性!!!
- 可以加上
explicit
关键字,防止隐式的转换,没有重载流操作符的情况下,流操作时有可能自动转换成可以流操作的类型,但是程序员却不知道!
class Test{
operator float(){
float ret;//转换之后的值
使用数据成员生成返回值ret
return ret;
}
}
int main(){
Test a;
float f;
f = a;//自动调用重载函数,实现赋值!
return 0;
}
重载 [ ] 运算符
实现像操作普通数组一样操作对象数组,能够实现更加精细的下标控制、越界检查;
- 返回值一定是引用类型,因为这个引用对象可能出现在左部;
class Test{
int & operator[](const int & sub){//形参指示是第几个元素
return 某个数组数据成员的 [sub] 元素;
}
}
//调用
int main(){
Test a;
a++;
a.operator++(0);//等价,形参符合定义类型的任意值
return 0;
}
总结
封装抽象类,实例化过程中我们希望调用者对对象的使用能符合像基本数据类型一样的使用习惯,通过主要是通过重载,实现同一个接口多种功能;通过重载运算符,隐式的实现对象之间的运算!!!