1、类的6个默认成员函数
在上一篇博客中我谈到了空类的大小是1,但是空类里面真的什么都没有吗,其实每个类里面都有6个默认成员函数,如果我们不写的话,系统会为我们默认生成。
这六个默认成员函数分别是:
构造函数:主要完成初始化工作
析构函数:主要完成资源的清理
拷贝构造:使用一个同类对象初始化另一个对象
赋值重载:将一个同类对象赋值给另一个对象
最后两个是普通对象和const对象取地址,这两个一般都直接用系统默认生成的,不用自己实现。
2、构造函数
概念
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022,9,28);
d1.Display();
system("pause");
return 0;
}
以上就是一个日期类的构造函数,这里使用了缺省参数,下面介绍构造函数的特性。
特性
1、构造函数主要任务并不是开空间给对象,而是初始化对象。
2、构造函数的函数名必须与类名相同
3、构造函数没有返回值
4、对象实例化时会自动调用构造函数
5、构造函数可以重载
class Date
{
public:
//有参的构造函数
Date(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
}
//无参的构造函数
Date()
{
_year = 2022;
_month = 9;
_day = 28;
}
void Display()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(100,2,2);
d1.Display();
//d2后面不要带(),否则就会被认定为函数调用,而我们本意是创建对象
Date d2;
d2.Display();
system("pause");
return 0;
}
上面的程序中有参的构造函数千万不能写成全缺省的构造函数,因为一个函数中不能存在多个默认构造函数。
默认构造函数有三种:
①我们不写,编译器默认生成的构造函数,默认生成的是无参的
②我们写的全缺省的默认构造函数
③我们写的无参的构造函数
6、编译器默认生成的构造函数初始化的值是随机值
class Date
{
public:
//Date()
//{
// _year = 2022;
// _month = 9;
// _day = 25;
//}
private:
int _year;
int _month;
int _day;
};
int main()
{
//只有调用有参的时候才需要加(),无参一定不要加()
Date d1;
return 0;
}
7、对于内置类型(int、double、char、short等等)编译器才会去自动调用构造函数,而对于自定类型(我们自己用类、结构体等创建出的对象)会去调用他们自己的构造函数。
class A
{
public:
A()
{
cout << "A()" << endl;
}
};
class B
{
public:
B()
{
cout << "B()" << endl;
}
private:
int _n;
A a; //B类中包含一个A类对象
};
int main()
{
B b;
return 0;
}
可以看到先调用的是自定义类型的构造函数,后调用内置类型,这里不会因为声明顺序而改变两个构造函数的调用顺序。
8、成员变量的命名风格
通过上面的示例可以看到我定义的成员变量前都加了 _ ,这是为了避免与局部变量同名的情况,而且这样一眼就能看出这是成员变量。
9、构造函数不一定非要在函数体内初始化,还可以通过初始化列表初始化,并且对于常量、自定义类型这种必须通过初始化列表初始化
class A
{
public:
A(int a = 0)
{
_aa = a;
}
//private:
int _aa;
};
class B
{
public:
B(int a, int b, int c)
:_a(a)
,_b(b)
,_c(c)
{}
//这里为了方便打印,所以不加私有属性
//加上私有属性,类外不能直接访问
//private:
int _b;
const int _c;
A _a;
};
int main()
{
B b(1, 2, 3);
cout << b._a._aa << b._b << b._c << endl;
return 0;
}
3、析构函数
概念
上面提到析构函数是完成对象资源的清理,析构函数不是完成对象的销毁,销毁工作是编译器完成的,而对象在销毁时会自动调用构造函数去完成资源的清理。
特性
1、析构函数函数名就是类型前加上字符 ~
2、析构函数没有返回值
3、一个类中只能有一个析构函数,如果没有写,编译器会自动生成析构函数
4、当对象销毁时,编译器会自动调用析构函数完成资源清理
class Arr
{
public:
Arr(int capacity)
{
_p = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
cout << "Arr()" << endl;
}
~Arr()
{
if (_p != nullptr)
{
free(_p);
_p = nullptr;
}
_size = _capacity = 0;
cout << "~Arr()" << endl;
}
private:
int* _p;
int _size;
int _capacity;
};
int main()
{
Arr a(10);
return 0;
}
5、对于自定义类型,编译器也会像构造函数一样,会去调用他们自己的析构函数。
6、编译器默认生成的构造函数只会进行简单的清理工作,像在堆上开辟的空间,不会清理,所以可能造成内存泄漏,这种情况最好自己写析构函数。
4、拷贝构造函数
概念
拷贝构造函数也可以用来初始化对象,其作用是将一个已经实例化的本类对象去创建一个新的对象。
特征
1、拷贝构造函数是构造函数的一个重载形式
2、拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
下面是正确写法:
class A
{
public:
A(int capacity)
{
_size = 0;
_capacity = capacity;
}
A(const A& a)
{
_size = a._size;
_capacity = a._capacity;
}
private:
int _size;
int _capacity;
};
int main()
{
A a(10);
A b(a);
return 0;
}
3、如果没有显示定义,那么到需要调用拷贝构造时,系统会去调用默认生成的
可以看到系统默认生成的也能完成拷贝。
4、 编译器默认生成的拷贝构造是值拷贝,俗称浅拷贝,当成员变量有指针时,浅拷贝就不能用了,这时就得写一个深拷贝。
现在就写一个深拷贝来解决这种问题
class A
{
public:
A(int capacity)
{
_p = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
A(const A& a)
{
//重新开辟一块和 a 一样大小的空间
_p = (int*)malloc(sizeof(int) * a._capacity);
_size = a._size;
_capacity = a._capacity;
}
~A()
{
if (_p != nullptr)
{
free(_p);
_p = nullptr;
}
_size = _capacity = 0;
}
private:
int* _p;
int _size;
int _capacity;
};
int main()
{
A a(10);
A b(a);
return 0;
}
5、赋值运算符重载
C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类 型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名字:operator后面跟上想要重载的运算符
函数语法:返回值类型 operator操作符(参数列表)
注意:
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型或者枚举类型的操作数
- 重载函数的形参第一个默认是this,如果该运算符是单目操作符,形参列表就不要添加了
- .* 、:: 、sizeof 、?: 、. 注意以上5个运算符不能重载。
如果没有显示定义赋值运算符重载,编译器也同要会默认生成一个,完成的也是简单的值拷贝
下面写一个常用的赋值 = 运算符的重载(深拷贝)
class A
{
public:
A(int capacity)
{
_p = (int*)malloc(sizeof(int) * capacity);
_size = 0;
_capacity = capacity;
}
A& operator=(const A& a)
{
//避免自己给自己赋值,提高效率
if (this != &a)
{
//先释放自身_p申请的空间
free(_p);
//然后申请一个新空间
_p = (int*)malloc(sizeof(int) * a._capacity);
_size = a._size;
_capacity = a._capacity;
}
return *this;
}
~A()
{
if (_p != nullptr)
{
free(_p);
_p = nullptr;
}
_size = _capacity = 0;
}
private:
int* _p;
int _size;
int _capacity;
};
int main()
{
A a(10);
A b = a;
return 0;
}
6、const成员
cosnt修饰的成员函数
将const修饰的类成员函数称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this 指针,表明在该成员函数中不能对类的任何成员进行修改。
class B
{
public:
B(int b, int c)
:_b(b)
,_c(c)
{}
/*void Print()
{
_b = 20;
_c = 30;
cout << this->_b << this->_c << endl;
}*/
//此时参数列表隐含的this指针就变成了const B* this
void Print()const
{
//_b = 20;//err
//_c = 30;//err
cout << this->_b << this->_c << endl;
}
private:
int _b;
int _c;
};
int main()
{
B b(2, 3);
b.Print();
system("pause");
return 0;
}
总结:
1、函数后加const表示参数列表的参数都是常量
2、函数前加const表示函数返回值是常量
3、const对象不能调用非const函数,这属于权限的放大
4、非const对象可以调用const函数,这属于权限的缩小
5、const对象可以调用const函数,非const对象可以调用非const函数
7、取地址及const取地址操作符重载
这两个默认成员函数一般不怎么使用,而且编译器默认生成的也够用
class Date
{
public:
Date* operator&()
{
return this;
}
const Date* operator&()const
{
return this;
}
private:
int _year; // 年
int _month; // 月
int _day; // 日
};