思考问题:
1.空类什么都没有吗?
2.6个默认函数的相关知识
3.默认构造函数的作用?
3.拷贝构造与浅拷贝
4.const关键字的作用 与使用
目录
3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?
引言:
空类什么都没有吗?不是什么都没有,编译器会给类生成6个默认的成员函数:
1.构造函数----->用来进行成员对象创建时的初始化工作
2.析构函数------>对象销毁时资源的清理工作
3.拷贝构造、赋值运算符的重载---->用来进行对象的拷贝
4.两个取地址&的重载
问:什么叫默认的成员函数?
答:用户没有自己写,编译器会自动生成;一旦用户显示提供了编译器不再生成。
一、构造函数
1.概念
我们知道在定义一个内置类型变量的时候,我们可以对其进行初始化,如下:
int main()
{
int a = 10;
int b(10);
return 0;
}
问:那我们在对一个类类型的变量在创建的时候也可以默认的带上初始值可以吗?(强调一下,这里不是初始化,初始化后面会谈)
答:当然可以,我们也可以在创建变量的时候对其进行赋值,这就是我们构造函数的作用了。
如下:假如没有构造函数的话,我们在创建变量进行赋值的时候,需要自己调用Init函数进行赋值。很麻烦。
class Date
{
public:
void Init(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d;
//调用Init函数来进行赋值
d.Init(2022, 3, 14);
return 0;
}
但当我们有了拷贝构造函数之后
如上图,我们不需要再进行手动的调用Init函数,是不是方便了很多呢?
通过反汇编我们发现,编译器在创建对象的时候,自动帮我们调用了拷贝构造函数。
相信看了上面的格代码,也都大概知道构造函数该如何写了把!!
2.特性
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是需要注意的是构造函数的主要作用并不是开辟空间创建对象,而是初始换对象。
其特征如下:
- 函数名与类名相同
- 无返回值
- 对象实例化时编译器自动调用对应的构造函数
- 构造函数可以重载
class Date
{
public:
//无参构造函数
Date()
{}
//有参构造函数
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
//调用无参构造函数
Date d1;
//调用有参构造函数
Date d2(2022, 3, 24);
// 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
// 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
return 0;
}
- 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成
如上图,我们没有自己写构造函数,则编译器帮我们生成了默认的无参的构造函数。那么当我们写了之后呢?
当我们自己写了之后,编译器不再生成默认的拷贝构造函数。
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参 构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
默认的拷贝构造函数到底有什么用?
问:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象year/month/_day,依旧是随机值。
答:C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如int/char...,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
class Date
{
private:
// 基本类型(内置类型)
int _year;
int _month;
int _day;
// 自定义类型
Time _t;
};
int main()
{
Date d;
return 0;
}
如上图我们发现,我们明明创建的是Date对象,但是我们却调用了Time 的构造函数,很显然这是我们在创建自定义类型变量_t的时候调用的,
这是不是方便了很多的,我们可以实现在一个类对象中创建另一个类对象了。
为了证明上面的结论,我们查看反汇编:
总结:编译器生成的默认构造函数
1.对于内置类型的成员变量依旧为随机值。
2.对于自定义类型的对象,编译器就必须在生成的默认成员函数中调用内部自定义成员变量对应的无参构造函数。
全缺省的构造函数
二、析构函数
1.概念
通过前面的只是我们知道了一个对象是怎么来的,那一个对象又是怎么没的呢?
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的,而对象在销毁时会自当调用析构函数,完成类的一些资源清理工作。
2.特征
-
析构函数名是在类名前加上字符~;
-
无参数,无返回值;
-
一个类有且只有一个析构函数。若未显示定义,系统会自动生成默认的析构函数
-
对象生命周期结束时,C++编译系统自动调用析构函数
看到这里,细心的人发现,C中的一些有资源申请的代码,在类类型对象中不需要我们自己释放空间了,没错,因为这些事析构函数会帮我们做。
是不是方便了很多呢?如下代码:
typedef int DataType;
class SeqList
{
public:
SeqList(int capacity = 10)
{
_pData = (DataType*)malloc(capacity * sizeof(DataType));
assert(_pData);
_size = 0;
_capacity = capacity;
}
~SeqList()
{
if (_pData)
{
free(_pData); // 释放堆上的空间
_pData = NULL; // 将指针置为空
_capacity = 0;
_size = 0;
}
}
private:
int* _pData;
size_t _size;
size_t _capacity;
};
int main()
{
SeqList s;
return 0;
}
-
关于编译器自动生成的析构函数,是否会完成一些事情呢?下面的程序我们会看到,编译器生成的默认析构函数,对会自定类型成员调用它的析构函数。
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
class Person
{
private:
String _name;
int _age;
};
int main()
{
Person p;
return 0;
}
因为与构造函数类似,所以不再多做测试。
三、拷贝构造函数
1.概念
在现实生活中,可能存在一个与你一样的自己,我们称其为双胞胎。那在创建对象时,可否创建一个与一个对象一模一样的新对象呢?
当然可以。这时就轮到我们的拷贝构造函数出场了。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般使用const修饰),再用已存在的类类型队形创建新对象时由编译器自动调用。
2.特征
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个且必须使用引用传参,传值方式会引发无穷递归调用。
- 若未显示定义,系统生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
这是就有人说了,既然编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了 ,我们不就不用自己实现了吗?直接使用默认的不就行了吗?
当然不会这么简单的啦,看下面的函数。
// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
class String
{
public:
String(const char* str = "jack")
{
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
}
~String()
{
cout << "~String()" << endl;
free(_str);
}
private:
char* _str;
};
int main()
{
String s1("hello");
String s2(s1);
}
这就是因为我们默认的拷贝构造函数是浅拷贝的原因了。
浅拷贝的概念:
拷贝就是拷贝指向对象的指针,意思就是说:拷贝出来的目标对象的指针和源对象的指针指向的内存空间是同一块空间,浅拷贝只是一种简单的拷贝,让几个对象公用一个内存,然而当内存销毁的时候,指向这个内存空间的所有指针需要重新定义,不然会造成野指针错误。
从浅拷贝的概念我们就可以知道, 浅拷贝只是一种简单的拷贝,让几个对象公用一个内存,所以我们在调用析构函数释放空间的时候,将同一块空间重复释放,所以就造成了重复释放的错误,导致了程序的崩溃。那么没有解决的方法了吗?当然有,深拷贝,我们后面会学到。
四、 赋值运算符的重载
引言:
我们在C语言的时候,比较两个变量的时候我们可以直接使用“>”,"<","=="等运算符直接进行比较,很方便:
int main()
{
int a = 10;
int b = 20;
if (a > b){
return a;
}
return b;
}
那么对于类类型的对象,我们直接进行比较,就会出错,如下:
class Date
{
public:
Date(int year,int month,int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
bool Test()
{
Date d1(1999, 11, 11);
Date d2(2022, 3, 24);
if (d1 > d2){
return true;
}
return false;
}
int main()
{
Test();
return 0;
}
这就很让人烦恼,难道我们要比较两个类类型的对象必须要每次都通过调用函数来进行比较吗?不可以向内置类型那样的比较进行比较吗?
当然不是,我们类类型的对象也可以像内置类型一样进行比较。
1.运算符重载
-
不能通过连接其他符号来创建新的操作符:比如 operator@
-
重载操作符必须有一个类类型或者枚举类型的操作数
-
用于内置类型的操作符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义
-
作为类成员的重载函数时,其形参看起来比操作数数目少 1 成员函数的操作符有一个默认的形参 this ,限定为第一个形参
-
.* 、 :: 、 sizeof 、 ?: 、 . 注意以上 5 个运算符不能重载。这个经常在笔试选择题中出现。
当我们学习了运算符的重载之后我们就可以写下这样的代码了:(注:为了保证封装性,我们直接将重载函数作为成员函数)
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
//为了保证封装性,我们直接将重载函数作为成员函数
// bool operator==(Date* this, const Date& d2)
// 这里需要注意的是,左操作数是this指向的调用函数的对象
bool operator==(const Date& d2)
{
return _year == d2._year
&& _month == d2._month
&& _day == d2._day;
}
private:
int _year;
int _month;
int _day;
};
void Test()
{
Date d1(2018, 9, 26);
Date d2(2018, 9, 27);
cout << (d1 == d2) << endl;
}
int main()
{
Test();
return 0;
}
如上图,我们可以直接将两个类类型的对象进行比较了,哇,真的神奇。
2.赋值运算符重载
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
private:
int _year;
int _month;
int _day;
};
赋值运算符主要有四点:
-
参数类型
-
返回值
-
检测是否自己给自己赋值
-
返回 *this
-
一个类如果没有显式定义赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
五、const成员
1.const修饰类的成员函数
将const修饰类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。
我们来看看下面的两段代码 并解决问题:
1. const对象可以调用非const成员函数吗?
2. 非const对象可以调用const成员函数吗?
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// const类型的成员函数
// 在const成员函数中,不能修改当前对象中的"成员变量"
// this指针的类型:const Date* const this
void Print()const
{
// _day = -1;
cout << _year << "-" << _month << "-" << _day << endl;
}
// 普通成员函数
// 在其内部可以对当前对象修改 也可以不修改
void TestFunc()
{
_day += 1;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
/*
1. const对象可以调用非const成员函数吗?
2. 非const对象可以调用const成员函数吗? 可以的
*/
// 普通类型对象,对于const成员函数和普通成员函数都可以调用
// 因为:普通对象本来就是可读可写一个对象
Date d1(2022, 3, 20);
d1.Print();
d1.TestFunc();
const Date d2(d1); // d2对象本来就是只读的----只能读取d2对象中的内容,不能对d2对象进行写入
d2.Print(); // const对象可以调用const成员函数
// d2.TestFunc(); // const对象不能调用普通的成员函数---因为:在普通的成员函数中可能会对对象进行修改
return 0;
}
3. const成员函数内可以调用其它的非const成员函数吗?
4. 非const成员函数内可以调用其它的const成员函数吗?
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// this指针类型:Date* const
// 可读可写的一个函数
void f1() // 在成员函数中,可以对this指向对象中的内容进行修改
{
_day -= 1;
}
// this指针的类型: const Date* const
// 只读函数
void f2()const // 在成员函数中不能对this指针的当前对象中的成员变量进行修改
{
// _day -= 1;
}
// 1. const成员函数内可以调用其它的非const成员函数吗? 只能调用const成员函数
void TestConstFunc()const // 只读的成员函数
{
f2();
// f1(); // 编译失败:this指向的是一个只读的对象,如果让只读对象调用可读可写的成员函数,代码不安全
}
// 2. 非const成员函数内可以调用其它的const成员函数吗?
void TestFunc() // 可读可写成员函数
{
f1(); //
f2(); // this指向的对象本来就是可读可写的一个对象,调用const成员函数只是将对象中的内容读取,安全的
}
// 为了提高程序的安全性,如果成员函数内部确定一定会修改当前对象中的成员变量,最好将该成员函数设置为const类型的成员函数
void Print()const
{
// _day = -1;
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
对于上面几个问题我们可以得到如下答案: