1.构造函数
什么是构造函数?
构造函数是一个特殊的函数,名字与类名相同,创建类类型的对象的时候由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且在对象的生命周期内只能调用一次。
构造函数的特性
1.函数名与类名相同
2.没有返回值(就是在类名的前面不加任何的类型,void都不可以)
3.构造函数可以重载
4.对象创建的时候会自动调用的对应的构造函数
class Date
{
public:
//下面的两个构造函数构成重载
Date(int year)//这有一个形参的构造函数(类名的前面不加任何的类型)
{
_year = year;
}
Date(int year, int mouth, int day)//有三个形参的构造函数
{
_year = year;
_mouth = mouth;
_day = day;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1(2018);//调用第一个构造函数
Date d2(2011, 6, 6);//调用第二个构造函数
return 0;
}
5.构造函数可以在类内定义也可以在类外定义
class Date
{
public:
Date(int year);//在类内进行声明
private:
int _year;
int _mouth;
int _day;
};
Date::Date(int year)//在类外进行实现
{
_year = year;
}
int main()
{
Date d1(2018);//调用第一个构造函数
return 0;
}
6.如果类中没有显示定义一个构造函数的话,则c++编译器会自动的生成一个无参的默认构造函数,一旦用户在类中定义了一个构造函数的话,编译器将不会自动生成一个构造函数。
7.无参的构造函数和全缺省的构造函数被统称之为缺省构造函数,并且缺省构造函数只能有一个。
class Date
{
public:
Date()//没有参数的构造函数
{
;
}
Date(int year = 2018, int mouth = 6, int day = 10)//全缺省参数的构造函数
{
_year = year;
_mouth = mouth;
_day = day;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;//如果你不穿任何参数,这样调用的时候就会出错,因为编译器不知道调用那个构造函数
return 0;
}
2、析构函数
什么是析构函数?
析构函数与构造函数的功能相反,在对象被销毁之前,由编译器自动的调用,用来做一些类的资源清理和汕尾的工作。
注意: 析构函数体内不是删除对象,而是仅仅做一些对象删除前的相关清理工作。
析构函数的特性:
1.析构函数没有参数没有返回值。(因为析构函数是在对象删除时调用的,这时候传参数是没有意义的,相当于一个人吃饱饭之后,析构函数相当于帮他收拾餐具而不是再给他食物,因为他已经饱了,所以给他食物是没有意义的)
2.一个类中析构函数只能有一个,如果用于没有自己定义的话,编译器就会自动生成一个析构函数。反之编译器不会自己生成一个析构函数而是调用用于自己写的析构函数。
3.析构函数是在类名的前面加一个~
4.对象在生命周期结束的时候编译器会自动调用析构函数,而不需要用户自己调用。
class student
{
public:
student()//构造函数
{
_name = (char *)malloc(12);
_sex = (char *)malloc(4);
if(_name == NULL || _sex == NULL)
{
assert(0);
}
_age = 0;
}
~student()//析构函数在类名的前面加~。析构函数是没有参数和返回值的
{
if(_name)
{
free(_name);
_name = nullptr;
}
if(_sex)
{
free(_sex);
_sex = nullptr;
}
_age = 0;
}
private:
char *_name;
char *_sex;
int _age;
};
int main()
{
student s1;
return 0;
}
3、拷贝构造函数
什么是拷贝构造函数?
拷贝构造函数只有单个形参,而且这个参数必须是类类型的引用,再用已存在的类类型的对象创建新对象自时动调用。
class Date
{
public:
Date(int year = 2018, int mouth = 6, int day = 10)
{
_year = year;
_mouth = mouth;
_day = day;
}
Date(const Date& d)//只有一个形参,而且是类类型的引用,在前面一般(不是一定)加一个const,因为我们现在不需要改变d1中的内容
{
_year = d._year;
_mouth = d._mouth;
_day = d._day;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1(2000, 1, 1);
Date d2(d1);
return 0;
}
为什么拷贝构造函数要传一个引用而不是一个对象?
因为如果传值的话,会导致无穷递归,具体的如下图所示:
而如果传的时引用的话,相当于传的时对象的地址,这样就不会创建一个临时的对象。也就不会引发无穷递归。
拷贝构造函数的特征
1.拷贝构造函数是构造函数的一种重载形式。
2.拷贝构造函数的参数只有一个且必须是引用传参,传值的方式会引发无限的递归调用
3.如果没有显示的给出定义,那么系统会默认的生成一个拷贝构造函数。默认的拷贝构造函数会按照成员的声明顺序依此拷贝类的成员进行初始化。
那些类的拷贝构造函数一定要用户提供?
如果对象中有资源的话(比如动态开辟一块空间)一定要给出拷贝构造函数。
4、赋值运算符的重载
如果你想比较两个类类型的对象的大小,应该怎么办?
一般的时候想到的就是定义一个函数来比较,但是这样的方式不是很直观。用户不一定一眼就可以看出你的用途。
这时我们可以用运算符的重载来实现。代码如下:(实现一个==运算符的重载)
class Date
{
public:
Date(int year = 2018, int mouth = 6, int day = 10)
{
_year = year;
_mouth = mouth;
_day = day;
}
bool IsSameDate(const Date & d)//一般情况下用函数来比较
{
return _year == d._year && _mouth == d._mouth && _day == d._day;
}
bool operator==(const Date & d)//运算符的重载
{
return _year == d._year && _mouth == d._mouth && _day == d._day;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1(2000, 1, 1);
Date d2(d1);
//if(d1.IsSameDate(d2)) //这样比较的话,可读性不高,用户不能一眼看出来它的意思
if(d1 == d2)//这样比较非常直观,很容易就直到它的意思
{
cout<<"这是日期相同的一天"<<endl;
}
else
{
cout<<"这两个日期不相同"<<endl;
}
return 0;
}
什么是运算符的重载?
运算符是具有特殊函数名的函数,也具有其返回值的类型,函数的名字、参数列表、返回值的类型与普通的函数类似,函数的名字是:operator后面加需要重载的运算符符号。
运算符的重载可以重载成全局函数(当成员变量为公有的话)也可以重载成类的成员函数
注意:
1.不能通过连接其他的符号来创建新的运算符:比如:operator@;
2.重载运算符必须有一个是类类型或者是枚举类型的操作数。比如不能重载两个整形的加法运算符。
3.用于内置类型的操作符,其含义不能改变。
4.作为类的成员函数的重载函数,其形参看起来比操作数的数目少一位成员函数的,操作符有一个默认的形参this,限定为第一个形参。
赋值运算的重载
赋值运算符的重载
1.参数类型
2.返回值
3.检测是否自己给自己赋值
4.返回this
class Date
{
public:
Date(int year = 2000, int mouth = 10, int day = 1)
{
_year = year;
_mouth = mouth;
_day = day;
}
Date & operator=(Date & d)//可以返回引用的时候就尽量返回引用
{
if(this != &d)//避免自己给自己赋值
{
_year = d._year;
_mouth = d._mouth;
_day = d._day;
}
return *this;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1(2018,2,2);
Date d2;
d2 = d1;
return 0;
}
对于上面的运算符的重载函数来说,为什么要有返回值,为什么返回值是*this呢?
其实如果是两个元素之间的赋值的话,不用返回也可以,但是如果是几个元素的连续赋值的话,就不行了.
比如 a = b = c
对于上面这个连续赋值的操作来说,可以拆分为两个部分:b = c, a = b.首先是将c的值赋给b,如果没有返回值的话,后面的赋值运算就会出错,当然也不可以返回形参d,因为形参d相当于c,如果将c返回去的话,后面的赋值运算就不是a = b 而是a = c,这样虽然数值上使对的,但是逻辑上是错的。,所以只能返回*this;
现在我们再来看几种运算符的重载(++a,a++)
++a和a++的运算符的重载为:
class Data
{
public:
Data(int data = 0)
{
_a = data;
}
Data & operator++()//这是++a的操作
{
_a += 1;
return *this;
}
Data operator++(int)//这是a++的操作
{
Data tmp(*this);
_a += 1;
return tmp;
}
private:
int _a;
};
int main()
{
Data d(10);
++d;
d++;
return 0;
}
有上面的操作可知,++a和其他的操作符的的运算符的重载没有区别,但是a++有一定的不一样,如果a++按照前面的方式的话,会和++a的运算符函数重载的参数列表一样,所以为了区别,给了a++函数了加了一个形参,让两个函数形成重载。
下面的一段代码有问题吗?
typedef int DataType;
class SeqList
{
public:
SeqList(int capacity = 10)
{
Sq = (DataType *)malloc(sizeof(DataType)*capacity);
if(Sq == NULL)
{
assert(0);
}
_capacity = capacity;
_size = 0;
}
~SeqList()
{
free(Sq);
Sq = NULL;
_capacity = 0;
_size = 0;
}
private:
DataType *Sq;
int _capacity;
int _size;
};
int main()
{
SeqList s1(20);
SeqList s2(s1);
SeqList s3;
s3 = s1;
return 0;
}
这段代码是有问题的,首先如果一个类中涉及到资源(比如:动态内存开辟空间),一定要自己给出析构函数、拷贝构造函数、赋值运算符函数。 上面的代码没有自己给出的拷贝构造函数和赋值运算函数。所以会导致两个问题:
1.因为上面的代码中没有拷贝构造函数,所以s2调用系统默认生成的拷贝构造函数时,s2和s1共用了一块空间,这也导致了,当s1和s2在对象销毁时,调用析构函数释放空间时,一块空间释放了两次,所以会出错。
2.还有一个问题就是,当s3调用系统默认的赋值运算函数时,s3就会和s1指向同一块空间,而s3原来的这块空间就会丢失,导致s3原来的空间没有释放而造成内存泄漏。
刚开始创建s3的时候:
当执行s3 = s1这条语句的时候:
此时的s3指向的是s1的空间,而s3原来的空间就丢了。
5.const成员
首先我们来了解一下const,在c++中如果一个const修饰一个变量的话,这个变量就成为了一个常量,具有宏的属性,在编译期间会将const所修饰的常量进行替换。
const修饰类的成员函数:
const修饰的成员函数是如何写的呢?
class Data
{
public:
Data(int data = 0)
{
_a = data;
}
void SetData(int a)const//const修饰成员函数
{
;
}
private:
int _a;
};
是不是觉得这个const位置有点怪呢?是不是觉得(const void SetData(int a))这样才是正确的呢?
其实不能将const放在类型的前面,因为放在前面修饰的不是成员函数,而是这个函数的返回值。
但是,你会发现用const修饰函数中不能修改类中普通的“成员变量”,因为如下:
此时const修饰的是*this,所以this指针指向的内容不能改变。
如果的const修饰的成员函一定要修改某个成员变量的话,你可以在这个变量的前面加multable这个关键字。代码如下:
class Data
{
public:
Data(int data = 0)
{
_a = data;
}
void SetData(int a)const//const修饰成员函数
{
_a = a;
}
private:
mutable int _a;//在前面加mutable
};
int main()
{
Data d(10);
d.SetData(2);
return 0;
}
下面是关于const修饰成员变量的几个问题:
1.const修饰的对象可以调用非const修饰的成员函数和const修饰的成员函数吗?
const修饰的对象可以调用const修饰的成员函数。
cons修饰的对象是不可以修改对象的内容,所以如果调用非const修饰的成员函数时,const成员函数可能会修改对象的内容,这样对对象来说是不安全的。
2.非const修饰对象可以调用非const修饰的成员函数和const修饰的成员函数吗?
普通类型的对象可以调用非const修饰的成员函数,并且对函数的内容可读可修改。
非const修饰的对象为什么可以调用const修饰的成员函数?因为非const修饰的对象如果不对const修饰的成修改的话,是可以的调用的。所以对const修饰的成员函数只能读,而不能修改。
3.const修饰的成员函数可以调用其他非const修饰的成员函数和const修饰的成员函数吗?
const修饰的成员函数可以调用其他的const修饰的成员函数。
const修饰的成员函数不能调用其他的非const修饰的成员函数。因为this指针的类型不同,这个函数的this指针的类型是const test *const this ,而非const修饰的成员函数的this指针为test *const this ,类型不同所以不能调用。
4.非const修饰的成员函数可以调用其他非const修饰的成员函数和const修饰的成员函数吗?
非const修饰的成员函数可以调用其他的非const修饰的成员函数。
非const修饰的成员函数可以调用其他的const修饰的成员函数,因为当前函数是可读可写的,调用const修饰的成员函数是只能读的,所以如果对const函数进行只读的操作是可以的。(其实相当于大官(非const修饰的成员函数)可以指挥小官(const修饰的成员函数),也可以让同级别的官(非const修饰的成员函数)帮忙)。
6.取地址及const取地址操作符的重载
这两个,默认的成员函数一般不用重新定义,编译器会重新生成。
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year;
int _mouth;
int _day;
};
int main()
{
Date d1;
const Date d2;
cout<< &d1 <<endl;
cout<< &d2 <<endl;
return 0;
}
上面的两个函数可以构成重载吗?
可以,因为他们的this指针的类型不同。
这两个运算符一般不需要重载,使用编译器生成默认的取地址的重载即可,只有特殊的情况下才需要重载,比如想让别人获取到指定的内容