C++必修:类与对象(二)

Betty’s Sweet 2024-07-14 15:05:12 阅读 82

✨✨ 欢迎大家来到贝蒂大讲堂✨✨

🎈🎈养成好习惯,先赞后看哦~🎈🎈

所属专栏:C++学习

贝蒂的主页:Betty’s blog

1. 构造函数

1.1. 定义

构造函数是一个特殊的成员函数,名字与类名相同, 创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内只调用一次。其特点如下:

函数名与类名相同。无返回值。对象实例化时编译器自动调用对应的构造函数。构造函数可以重载。

下面是一个日期类的构造函数

<code>class Date

{

public:

Date(int year, int month, int day)

{

_year = year;

_month = month;

_day = day;

}

void Print()

{

cout << _year << "-" << _month << "-" << _day << endl;

}

private:

int _year;

int _month;

int _day;

};

int main()

{

Date d1(1,1,1);//自动调用

d1.Print();

return 0;

}

img

构造函数的功能就相当于我们之前书写的初始化函数,但由于其自动调用的特性,大大提升了代码的容错率。

1.2. 注意

如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。

<code>class Date

{

public:

/*Date(int year, int month, int day)

{

_year = year;

_month = month;

_day = day;

}*/

//编译器会自动生成一个无参的默认构造函数

void Print()

{

cout << _year << "-" << _month << "-" << _day << endl;

}

private:

int _year;

int _month;

int _day;

};

无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。注意:无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数。

class Date

{

public:

Date()//无参

{

_year = 1900;

_month = 1;

_day = 1;

}

Date(int year = 1900, int month = 1, int day = 1)//全缺省

{

_year = year;

_month = month;

_day = day;

}

void Print()

{

cout << _year << "-" << _month << "-" << _day << endl;

}

private:

int _year;

int _month;

int _day;

};

int main()

{

Date d;//引起歧义

return 0;

}

img

当存在多个默认构造函数时,一旦我们对对象进行实例化,编译器不知道调用哪个构造函数,就会引起歧义。

编译器生成的默认构造函数只会对自定义类型(类)进行初始化,内置类型(int,double…)不会进行初始化,即调用自定义类型的构造函数。

<code>class Betty

{

public:

Betty()

{

cout << "Betty" << endl;

}

private:

int _a;

};

class Date

{

public:

void Print()

{

cout << _year << "/" << _month << "/" << _day << endl;

}

private:

Betty b;

int _year;

int _month;

int _day;

};

int main()

{

Date d;

d.Print();

return 0;

}

img

从上述实例观察,编译器自动生成的默认构造函数的确只对自定义类型进行初始化。

**特别注意:**C++11 中针对内置类型成员不初始化的缺陷,又进行了优化,即:内置类型成员变量在类中声明时可以给默认值

<code>class Date

{

public:

void Print()

{

cout << _year << "/" << _month << "/" << _day << endl;

}

private:

int _year = 1;//缺省值

int _month = 1;//缺省值

int _day = 1;//缺省值

};

int main()

{

Date d;

d.Print();

return 0;

}

img

2. 初始化列表

2.1. 定义

初始化列表作用与构造函数类似,它是在构造函数中以一个冒号开始,接着是以逗号分隔的数据成员列表,每个数据成员后面跟一个放在括号中的初始化式。

下面我们还是以一个日期类来示范:

<code>class Date

{

public:

Date(int year, int month, int day)

: _year(year)

, _month(month)

, _day(day)

{

//...

}

void Print()

{

cout << _year << "-" << _month << "-" << _day << endl;

}

private:

int _year;

int _month;

int _day;

};

int main()

{

Date d(2024,1,3);

d.Print();

return 0;

}

img

2.2. 注意

每个成员变量在初始化列表中只能出现一次(初始化只能初始化一次)。类中包含以下成员,必须放在初始化列表位置进行初始化:引用成员变量,const成员变量,自定义类型成员(且该类没有默认构造函数时)。因为这些变量都需要在定义时初始化。

<code>class A

{

public:

A(int a)

:_a(a)

{ }

private:

int _a;

};

class B

{

public:

B(int a, int ref)

:_b(a)

, _ref(ref)

, _n(3)

{ }

private:

A _b; // 没有默认构造函数

int& _ref; // 引用

const int _n; // const常量

};

尽量使用初始化列表初始化,因为不管你是否使用初始化列表,对于自定义类型成员变量,一定会先使用初始化列表初始化。

class A

{

public:

A(int a = 1)//默认构造

:_a(a)

{

cout << "A(int a = 1)" << endl;

}

private:

int _a;

};

class B

{

public:

B(int a)

:_m(a)

{ }

private:

int _m;

A _b;

};

int main()

{

B b(2);

return 0;

}

img

成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关

<code>class A

{

public:

A(int a)

:_a1(a)

, _a2(_a1)

{ }

void Print()

{

cout << _a1 << endl;

cout << _a2 << endl;

}

private:

int _a2;

int _a1;

};

int main()

{

A aa(1);

aa.Print();

}//输出??

如果是以初始化列表的顺序,那应该输出1和1。如果以声明顺序,那应该是1与随机值。

img

3. 析构函数

3.1. 定义

析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。其特点如下:

析构函数名是在类名前加上字符 ~。无参数无返回值类型。一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载对象生命周期结束时,C++编译系统系统自动调用析构函数。

下面是一个日期类的析构函数:

<code>class Date

{

public:

//构造函数

Date(int year = 1, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

void Print()

{

cout << _year << "-" << _month << "-" << _day << endl;

}

//析构函数

~Date()

{

_year = _month = _day = 0;

}

private:

int _year;

int _month;

int _day;

};

析构函数就相当于C语言中的销毁函数,但由于其自动调用的特性,大大提升了代码的容错率。

3.2. 注意

如果类中没有显式定义析构函数,则C++编译器会自动生成一个析构函数,一旦用户显式定义编译器将不再生成。

class Date

{

public:

//构造函数

Date(int year = 1, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

void Print()

{

cout << _year << "-" << _month << "-" << _day << endl;

}

//析构函数

/*~Date()

{

_year = _month = _day = 0;

}*/

//编译器会自动生成一个析构函数

private:

int _year;

int _month;

int _day;

};

编译器生成的析构函数对内置类型(int,double…)不会进行处理,对于自定义类型调用其析构函数。

class Betty

{

public:

~Betty()

{

cout << "~Betty" << endl;

}

private:

int _a;

};

class Date

{

public:

//构造函数

Date(int year = 1, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

void Print()

{

cout << _year << "-" << _month << "-" << _day << endl;

}

//默认生成

private:

Betty b;

int _year;

int _month;

int _day;

};

img

因为指针类型也属于内置类型,所以默认成员在动态内存开辟内存后,必须显式写成析构函数。不能靠编译器默认生成。

<code>typedef int DataType;

class Stack

{

public:

Stack(size_t capacity = 2)

{

_array = (DataType*)malloc(sizeof(DataType) * capacity);

if (nullptr == _array)

{

perror("malloc申请空间失败!!!");

return;

}

_capacity = capacity;

_size = 0;

}

void Push(DataType data)

{

// CheckCapacity();

_array[_size] = data;

_size++;

}

~Stack()

{

if (_array!=nullptr)

{

free(_array);

_array = nullptr;

_capacity = 0;

_size = 0;

}

}

private:

DataType* _array;

int _capacity;

int _size;

};

比如说上述代码,默认生成的析构函数并不会释放其内存,就可能造成内存泄漏。

4. 拷贝构造函数

4.1. 定义

**拷贝构造函数:**只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。其特点如下:

拷贝构造函数是构造函数的一个重载形式。

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;

}

private:

int _year;

int _month;

int _day;

};

int main()

{

Date d1(2024,2,2);

Date d2(d1);//拷贝构造

Date d3 = d1;//拷贝构造

d1.Print();

d2.Print();

d3.Print();

return 0;

}

img

4.2. 注意

拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

<code>Date(const Date d) //error

{

_year = d._year;

_month = d._month;

_day = d._day;

}

img

若未显式定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

<code>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;

//}

void Print()

{

cout << _year << "-" << _month << "-" << _day << endl;

}

private:

int _year;

int _month;

int _day;

};

int main()

{

Date d1(2024, 2, 2);

Date d2(d1);//拷贝构造

Date d3 = d1;//拷贝构造

d1.Print();

d2.Print();

d3.Print();

return 0;

}

img

因为编译器默认生成的拷贝构造函数是值拷贝,在某些场景下就会出错。比如说以下场景:

<code>typedef int DataType;

class Stack

{

public:

Stack(size_t capacity = 10)

{

_array = (DataType*)malloc(capacity * sizeof(DataType));

if (nullptr == _array)

{

perror("malloc申请空间失败");

return;

}

_size = 0;

_capacity = capacity;

}

void Push(const DataType& data)

{

// CheckCapacity();

_array[_size] = data;

_size++;

}

~Stack()

{

if (_array)

{

free(_array);

_array = nullptr;

_capacity = 0;

_size = 0;

}

}

private:

DataType* _array;

size_t _size;

size_t _capacity;

};

int main()

{

Stack s1;

s1.Push(1);

s1.Push(2);

s1.Push(3);

s1.Push(4);

Stack s2(s1);//默认拷贝构造

return 0;

}

img

为什么会出现这种情况呢?让我们看看下面这幅图:

img

因为默认生成的拷贝构造只是进行只拷贝,对于<code>size,capacity拷贝并不会出现问题,但是当s1的_array拷贝给s2的_array时,就会让s1与s2的同时指向同一片空间。而我们知道当对象的作用域结束时,会自动调用析构函数,同时对同一片空间析构两次,就会保错。所以当类中需要资源申请时,都需要手动写拷贝构造。

拷贝构造的应用场景有很多,能用引用尽量用引用,减少拷贝,提高程序效率。

5. 运算符重载

5.1. 定义

C++为了增强代码的可读性引入了运算符重载,运算符重载是具由运算符operator定义有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。该函数能让我们自定义类型像内置类型一样使用-+*/等运算符。

下面实现了简单判断日期是否相当的运算符重载:

class Date

{

public:

Date(int year, int month, int day)

: _year(year)

, _month(month)

, _day(day)

{

//...

}

void Print()

{

cout << _year << "-" << _month << "-" << _day << endl;

}

int _year;

int _month;

int _day;

};

bool operator == (const Date&d1,const Date& d2)

{

return d1._year == d2._year

&& d1._month == d2._month

&& d1._day == d2._day;

}

int main()

{

Date d1(2024,1,1);

Date d2(2024, 1, 1);

if (d1 == d2)//也可以显示调用operator==(d1,d2);

{

cout << "日期相等" << endl;

}

else

{

cout << "日期不相等" << endl;

}

return 0;

}

当然我们也可以将运算符重载声明在类中。

bool operator==(const Date& d)

{

return _year == d._year

&& _month == d._month

&& _day == d._day;

}

5.2. 注意

不能通过连接其他符号来创建新的操作符:比如operator@重载操作符必须有一个类类型参数用于内置类型的运算符,其含义不能改变,例如:内置的整型 + ,不 能改变其含义作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this.* sizeof ? : :: . 注意以上5个运算符不能重载。

6. 赋值运算符重载

6.1. 定义

赋值运算符重载是将运算符 =进行运算符重载。但是它相较于其他运算符重载有着自己独特的特点。

class Date

{

public:

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

Date& operator=(const Date& d)//赋值运算符重载

{

if (this != &d)

{

_year = d._year;

_month = d._month;

_day = d._day;

}

return *this;

}

private:

int _year;

int _month;

int _day;

};

参数类型:const T& ,传递引用可以提高传参效率。返回值类型:T& ,返回引用可以提高返回的效率,支持连续赋值。检测是否自己给自己赋值。返回* this :要复合连续赋值的值。

6.2. 注意

用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。

class Time

{

public:

Time()

{

_hour = 1;

_minute = 1;

_second = 1;

}

Time& operator=(const Time& t)

{

cout << "Time& operator=(const Time& t)" << endl;

if (this != &t)

{

_hour = t._hour;

_minute = t._minute;

_second = t._second;

}

return *this;

}

private:

int _hour;

int _minute;

int _second;

};

class Date

{

private:

// 基本类型(内置类型)

int _year = 2024;

int _month = 1;

int _day = 1;

// 自定义类型

Time _t;

};

int main()

{

Date d1;

Date d2;

d1 = d2;

return 0;

}

img

因为编译器默认生成默认赋值运算符重载的是值拷贝,在某些场景下就会出错。具体实例参考拷贝构造函数。赋值运算符只能重载成类的成员函数不能重载成全局函数。

<code>class Date

{

public:

Date(int year = 1900, int month = 1, int day = 1)

{

_year = year;

_month = month;

_day = day;

}

int _year;

int _month;

int _day;

};

Date& operator=(Date& left, const Date& right)//error

{

if (&left != &right)

{

left._year = right._year;

left._month = right._month;

left._day = right._day;

}

return left;

}

因为赋值运算符如果不显式实现,编译器会生成一个默认的赋值运算符重载。此时用户再在类外自己实现一个全局的赋值运算符重载,就会和编译器在类中生成的默认赋值运算符重载冲突。

7. const修饰函数

首先我们得知道一个规则就是,**const修饰的常变量不能赋值给普通变量,因为这样造成const权限的放大,但是普通变量可以赋值给const修饰的常变量。**所以让我们来看看这段代码:

class Date

{

public:

Date(int year, int month, int day)

{

_year = year;

_month = month;

_day = day;

}

void Print()

{

cout << "Print()" << endl;

cout << "year:" << _year << endl;

cout << "month:" << _month << endl;

cout << "day:" << _day << endl << endl;

}

private:

int _year; // 年

int _month; // 月

int _day; // 日

};

int main()

{

const Date d2(2022, 1, 13);

d2.Print();//error

return 0;

}

这段代码会出错,因为d2进行函数传参是将const Date*传过去,而函数接受参数的类型为Date*,这样就会造成权限的放大。为了解决这个问题,就需要使用const修饰原函数

void Print() const

{

cout << "Print()" << endl;

cout << "year:" << _year << endl;

cout << "month:" << _month << endl;

cout << "day:" << _day << endl << endl;

}

并且与原函数构成重载,可以同时存在。

8. 取地址及const取地址操作符重载

我们知道对自定义类型使用运算符需要对其进行重载,那么&自然也不例外。

class Date

{

public:

Date* operator&()

{

return this;

}

const Date* operator&()const

{

return this;

}

private:

int _year; // 年

int _month; // 月

int _day; // 日

};

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人获取到原有地址!



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。