1. 类的六个默认函数
如果一个类中什么成员都没有,简称为空类。空类中什么都没有吗?并不是的
类里面成员函数我们什么都不写的时候,编译器会自动生成6个函数,这6个函数就叫默认(缺省)成员函数
class Date {};
2. 构造函数
构造函数顶替的就是Init()函数,Init的风险是可能会没有初始化就使用该对象 C++为了解决这个问题也带来了构造函数
先不急,我们看看Java中的构造函数(构造器)
2.1 Java的构造器
构造方法又叫构造器(constructor),是类的一种特殊的方法,它的主要作用是完成对新对象的初始化。它有几个特点:
[修饰符] 方法名(形参列表){
方法体;
}
🍁 构造器的修饰符可以默认, 也可以是public protected private
🍁 构造器没有返回值
🍁 方法名和类名字必须一样
🍁 参数列表和成员方法一样的规则
🍁 构造器的调用, 由系统完成
2.1.2 Java构造器快速入门
🌿 方法名和类名相同
🌿 没有返回值
🌿 在创建对象时,系统会自动的调用该类的构造器完成对象的初始化。
public Person(String pName, int pAge) {
System.out.println("构造器被调用~~ 完成对象的属性初始化");
name = pName;
age = pAge;
}
2.1.3 Java构造器使用细节
🍁 一个类可以定义多个不同的构造器,即构造器重载
比如:我们可以再给Person类定义一个构造器,用来创建对象的时候,只指定人名,不需要指定年龄
🍁 构造器名和类名要相同
🍁 构造器没有返回值
🍁 构造器是完成对象的初始化,并不是创建对象
🍁 在创建对象时,系统自动的调用该类的构造方法
🍁 如果程序员没有定义构造器,系统会自动给类生成一个默认无参构造器也叫默认构造器)
🍁 一旦定义了自己的构造器,默认的构造器就覆盖了,就不能再使用默认的无参构造器,除非显式的定义一下
2.2 C++的构造函数
和Java很像
class Date{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 5, 24);
return 0;
}
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有 一个合适的初始值,并且在对象的生命周期内只调用一次。
2.2.1 C++构造函数快速入门
构造函数__是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务__并不是开空间创建对象,而是初始化对象。
🍁 函数名与类名相同。
🍁 无返回值。
🍁 对象实例化时编译器__自动调用__对应的构造函数。
🍁 构造函数可以__重载__。 Stack重载
class Stack{
public:
Stack()
{
_a = nullptr;
_size = _capacity = 0;
}
Stack(int capacity)
{
_a = (int*)malloc(sizeof(int)*capacity);
_size = 0;
_capacity = capacity;
}
private:
int* _a;
int _size;
int _capacity;
};
这样我们就可以把Init()彻底淘汰了
一般初始化分两种初始化,给定值的初始化和不给定值的初始化,其实可以合二为一,变成缺省函数,其中全缺省函数最好用了
Date重载
class Date{
public:
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
Date()
{
_year = 0;
_month = 1;
_day = 1;
}
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
🍁 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
🍁关于编译器生成的默认成员函数,在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象依旧是随机值
此处有坑,编译器会区别对待
对于C内置类型(基本类型),也就是语言原生定义的类型,比如:int,char,double,还有指针不初始化
对于自定义类型:class,struct等的定义的类型,编译器会去调用他们的默认构造器函数初始化
class Date
{
private:
int _year;
int _month;
int _day;
Time _t;
};
🍁 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认成员函数。
🍁 成员变量的命名风格(之前讲过带下划线)
小结:构造函数的细节很多,大多数情况下都要写构造函数完成初始化,建议写全缺省的构造函数
3. 析构函数
3.1 Intro of 析构函数
析构函数:与构造函数功能相反,析构函数不是完成对象的销毁,局部对象销毁工作是由编译器完成的。而__对象在销毁时会自动调用析构函数,完成类的一些资源清理工作__。
3.2 析构函数快速入门
析构函数的特点是: 🌿 析构函数名是在类名前加上字符 ~ 。
🌿 无参数无返回值。(不能重载)
🌿 一个类有且只有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。
🌿 对象生命周期结束时,C++编译系统系统自动调用析构函数。 使用完成后编译器会自动调用析构函数完成资源的清理 这个函数对Date类好像没什么用,但是对于Stack类是很有用的
class Stack
{
public:
Stack(int capacity = 4)
{
_a = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
void Push(int x)
{}
~Stack()
{
cout << "~Stack()析构函数" << endl;
free(_a);
_a = nullptr;
_size = _capacity = 0;
}
private:
int* _a;
int _size;
int _capacity;
};
当我们使用完这个Stack的时候,不用再去写一个StackDestroy,不用担心忘记在使用完之后调用Destroy函数,系统会在生命周期结束之后,在类里面找到析构函数,来清理
3.3 析构顺序
int main()
{
Stack st1;
st1.Push(1);
st1.Push(2);
st1.Push(3);
Stack st2;
st2.Push(10);
st2.Push(11);
st2.Push(12);
return 0;
}
上面的这段代码中谁先被析构呢?
4. 拷贝构造函数
在创建对象时,可否创建一个与一个对象一某一样的新对象呢?
Java 可以new一个对象=原对象即可
C++呢?
可以通过拷贝构造函数
Date d1(2020, 5, 26);
Date d4(d1);
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用
4.1 拷贝构造函数快速入门
拷贝构造函数也是特殊的成员函数,其__特征__如下: 🍁 拷贝构造函数是构造函数的一个重载形式 🍁 拷贝构造函数的__参数只有一个__且必须使用引用传参,使用__传值方式会引发无穷递归调用__。
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
为什么会发生无穷递归调用
所以说
🍁 传参的时候加上const,权限缩小
构造拷贝写错,下面是一段错误的代码,把要拷贝的和源写反了,会导致产生随机值,如何规避这个问题?
Date( Date& d)
{
d._year = _year;
d._month = _month;
d._day = _day;
}
Date d1(2020, 5, 26);
Date d4(d1);
方法就是加上修饰符const
Date(const Date& d)
{
d._year = _year;
d._month = _month;
d._day = _day;
}
🍁 若未__显示定义__,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷 贝,这种拷贝叫做浅拷贝,或者值拷贝。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
🍁 编译器生成的__默认拷贝构造函数__已经可以完成__字节序的值拷贝__了,我们还需要__自己实现__吗?像 日期类这样的类是没必要的。但是还有些类要自己去实现的
class Stack
{
public:
Stack(int capacity = 4)
{
if (capacity <= 0)
{
_a = nullptr;
_size = _capacity = 0;
}
else
{
_a = (int*)malloc(sizeof(int)*capacity);
_capacity = capacity;
_size = 0;
}
}
~Stack()
{
free(_a);
_size = _capacity = 0;
_a = nullptr;
}
private:
int* _a;
int _size;
int _capacity;
};
比如说这个类,假如说我拷贝构造了一次,但是因为里面有析构函数,析构函数中存在free(),当我析构的时候会产生问题,由于拷贝的时候是传引用,所以两个指针指向同一个间却需要被free两次,我们之前学过malloc的空间不能多次free,所以程序若运行会产生报错
还有一个问题是,这两者是不能共用同一个空间的,是其中一个对象插入删除数据会导致另外一个对象也被修改,因此像Stack类不适合编译器默认生成的浅拷贝
4.3 拷贝构造什么时候会被调用
在什么情况下编译器会调用拷贝构造函数:(三种情况)
🌸 用类的一个对象去初始化另一个对象时
Complex c1(c2);
Complex c1=c2;
注:下面这2种时候不调用
Complex c1,c2;
c1=c2;
🌸 当函数的形参是类的对象时(也就是值传递时),如果是引用传递则不会调用
🌸 当函数的返回值是类的对象时
5. 赋值运算符重载
5.1 运算符重载
内置类型,语言层面就支持运算符 自定义类型,默认不支持。C++可以用运算符重载来让类对象支持用某个运算符,需要那个就重载哪一个
C++为了增强代码的__可读性__引入了运算符重载,运算符重载是__具有特殊函数名的函数,也具有其返回值类 型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似__。 函数名字为:关键字operator后面接需要重载的运算符符号。
函数原型:返回值类型 operator操作符(参数列表)
bool operator==(Date d)
{
return _year == d._year
&& _month == d._month
&& _day == d._day;
}
int main()
{
d1 == d2;
return 0;
}
5.1.1 Java中同样效果的思考
联系上面的栗子,联想到了Java中的重写Equals方法和Hashcode
Java中,判断两个对象在逻辑上是否相等,如根据类的成员变量来判断两个类的实例是否相等,而继承Object中的equals方法只能判断两个引用变量是否是同一个对象。这样我们往往需要重写equals()方法。
我们向一个没有重复对象的集合中添加元素时,集合中存放的往往是对象,我们需要先判断集合中是否存在已知对象,这样就必须重写equals方法。
而C++重写的运算符,因为自定义类型不像内置类型可以直接比较,所以必须借助函数来比较,把它也写成== ,增加了可读性
5.1.2 运算符重载和函数重载有关系吗?
5.2 运算符重载快速入门
🍁 不能通过连接其他符号来创建新的操作符:比如operator@
🍁 重载操作符必须有一个类类型或者枚举类型的操作数
🍁 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义
🍁 作为类成员的重载函数时,其形参看起来比操作数数目少1成员函数的
🍁 操作符有一个默认的形参this,限定为第一个形参
🍁 .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载(* 可以,.* 不可以)
下面来一些🌰
判断日期<的重载
bool operator<(Date x)
{
if (_year < x._year)
{
return true;
}
else if (_year == x._year)
{
if (_month < x._month)
{
return true;
}
else if (_month == x._month)
{
if (_day < x._day)
{
return true;
}
}
}
return false;
}
int main()
{
cout << (d1 < d2) << endl;
return 0;
}
实现一个数组类
class Array
{
public:
Array()
{
for (int i = 0; i < 10; ++i)
{
_a[i] = i * 10;
}
}
int& operator[](size_t pos)
{
return _a[pos];
}
private:
int _a[10];
};
int main()
{
Array ay;
cout << ay[0] << endl;
cout << ay[1] << endl;
cout << ay[2] << endl;
cout << ay[3] << endl;
ay[0] = 100;
ay[1] = 200;
ay[2] = 300;
ay[3] = 400;
cout << ay[0] << endl;
cout << ay[1] << endl;
cout << ay[2] << endl;
cout << ay[3] << endl;
return 0;
}
5.3 赋值运算符重载
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
}
5.3.1 赋值运算符快速入门
赋值运算符有如下关键点
🍁 参数类型
🍁 返回值
🍁 检测是否自己给自己赋值
🍁 返回*this
🍁 一个类如果__没有显式定义__赋值运算符重载,编译器也会生成一个,完成对象按字节序的值拷贝。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(2018,10, 1);
d1 = d2;
return 0;
}
5.3.2 赋值运算符重载和拷贝构造函数一样吗
赋值拷贝
这里传参的时候传引用的原因是,如果直接传值调用的时候要调拷贝构造,不是一定要传引用,但是建议加上
void operator=(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
拷贝构造
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
🌿 不一样的是,拷贝构造是创建一个对象时,拿同类对象初始化的拷贝,赋值拷贝时两个对象已经都存在了,都被初始化过了,现在想把一个对象,赋值拷贝给另一个对象
🌿 一样的是,编译器默认生成赋值运算符跟拷贝构造的特性是一致的 🍒针对内置类型,会完浅拷贝,也就是说像Date这样的类不需要我们自己写赋值运算符重载,Stack就得自己写
? 🍒 针对自定义类型,也一样,它会调用他的赋值运算符重载完成拷贝
但是这里的= 赋值还不够好,类比内置类型,都有连续赋值的形式
对于i=j=k ,应该先是k赋给j,然后这个表达式返回值是j,把j再赋给i的形式,于是我们试着改写成连续赋值的形式
Date& operator=(const Date& d)
{
if (this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
返回值如果是对象的话会调用拷贝构造,为防止调用,用引用返回的方式,不过,要用引用作为返回值的话,除了作用域之后要保证还在才可以使用,这里还在,所以可以用
为了防止自己给自己赋值,加一个判断
6. 对前面知识点的总结:
C++的类中有6个默认成员函数,其中四个 🍁 构造函数 – 初始化,大部分情况下,都需要我们自己写构造函数
🍁 析构函数 – 清理内对象中资源
🍁 拷贝构造函数 – 拷贝初始化,特殊构造函数 深浅拷贝问题
🍁 赋值运算符重载 – 也是拷贝行为,但是不一样的是,拷贝构造是创建一个对象时,拿同类对象初始化的拷贝,赋值拷贝时两个对象已经都存在了,都被初始化过了,现在想把一个对象,赋值拷贝给另一个对象
针对我们不写编译默认生成的总结一下:
我们不写编译器会自动生成,虽然会自动生成,但是好多时候还是需要我们自己写,因为生成的那个不一定好用
🍁 构造和析构的特性是类似的,我们不写编译器内置类型不处理,自定义类型调用它的构造和析构处理
🍁 拷贝构造和赋值重载特性是类似的,内置类型会完成浅拷贝,自定义类型会调用他们的拷贝构造和赋值重载
7. const 成员
7.1 const修饰类成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中__不能对类的任何成员进行修改__。
7.2 const修饰成员函数快速入门
下面这种情况很容易出错
bool operator==(const Date& d)
{
return (_year = d._year)
&& (_month = d._month)
&& (_day = d._day);
}
一不小心把== 写成了= ,导致成员变量被修改,其实我们发现好像我已经写了const,但是这个const保护的是传入的对象的值,不能被修改,而不是this*指向的对象,保护对象
于是我们简易就利用好const修饰成员函数
bool operator==(const Date& d) const
{
return (_year == d._year)
&& (_month == d._month)
&& (_day == d._day);
}
这种就没有必要加,虽然可以运行,加也不是随便加,重点还有一个是注意?? 不能权限放大
void Print() const
{
cout << _year << "年" << _month << "月" << _day << "日" << endl;
}
7.3 回顾const修饰指针
语句\修饰项 | p1 | *p1 |
---|
const Date *p1 | F | T | Date const *p1 | F | T | Date *const p1 | T | F |
7.4 const修饰对象注意事项
🍁 const对象可以调用非const成员函数吗?
不可以,权限放大
🍁 非const对象可以调用const成员函数吗?
可以,权限缩小
🍁 const成员函数内可以调用其它的非const成员函数吗?
不可以,成员函数中,编译器处理以后在成员(成员变量/成员函数)前面都会加this->,所以普通成员函数可以调用其他的普通的成员函数,但是权限放大的话不可以
🍁 非const成员函数内可以调用其它的const成员函数吗?
可以,权限缩小
小结:
加const,修饰的是*this,好处:函数中不小心改变的成员变量,编译时就会被检查出来 建议:成员函数中,不需要改变成员变量,建议都加上const
8. 取地址及const取地址操作符重载
class Date
{
public:
Date(int year = 0, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date* operator&()
{
return this;
}
const Date* operator&() const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。
这两个运算符一般不需要重载,该功能不是特别有价值,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容!怎么弄呢,就是返回空指针
感谢阅读,干净又卫生,兄弟们
|