资讯

精准传达 • 有效沟通

从品牌网站建设到网络营销策划,从策略到执行的一站式服务

【C++】类和对象(中)-创新互联

文章目录
  • 1.类的6个默认成员函数
  • 2. 构造函数
    • 2.1概念与特征
    • 2.2特征分析
  • 3.析构函数
    • 3.1概念与特征
    • 3.2特征分析
  • 4.拷贝构造函数
    • 4.1概念与特征
    • 4.2特征分析
  • 5.运算符重载
    • 5.1运算符重载的概念
    • 5.2赋值运算符重载
      • 5.2.1特性
      • 5.2.2特性分析
  • 6.const成员&&取地址及const取地址操作符重载
    • 6.1const成员
    • 6.2取地址及const取地址操作符重载
  • 7、总结

青冈网站制作公司哪家好,找创新互联!从网页设计、网站建设、微信开发、APP开发、成都响应式网站建设公司等网站项目制作,到程序开发,运营维护。创新互联自2013年起到现在10年的时间,我们拥有了丰富的建站经验和运维经验,来保证我们的工作的顺利进行。专注于网站建设就选创新互联。
1.类的6个默认成员函数

如果一个类中没有任何成员,那么我们把它称为空类。空类中也会自动生成6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

image-20221024145647391

接下来我们详细介绍一下这几个默认成员函数

2. 构造函数 2.1概念与特征

在C语言实现的数据结构中,下面以栈(Stack)为例,我们实现过StackInit这个接口,用于对栈进行初始化,但是我们在使用栈的时候会经常忘记调用这个函数对栈初始化,C++为了解决这个问题,就创建了一个叫做构造函数的默认成员函数,用来对类进行初始化。而且它会在类实例化的时候自动调用,这就完美解决了我们忘记调用的问题。

这里需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

其特征如下:

  1. 函数名与类名相同。

  2. 无返回值。

  3. 对象实例化时编译器自动调用对应的构造函数。

  4. 构造函数可以重载和缺省参数。

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

  6. 编译器自动生成的构造函数对于内置类型不做任何处理,对于自定义类型调用其默认构造函数。

  7. 无参的构造函数、全缺省的构造函数和编译器默认生成的构造函数都称为默认构造函数,并且默认构造函数只能有1个。

    总结:不传参数就可以调用的构造函数,就叫默认构造函数

2.2特征分析

下面我们以Date类为例:

class Date
{public:
	Date(int year = 1970, 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 d1;
	d1.Print();
	return 0;
}

在定义构造函数的时候,函数名与类名相同(特征1),并且没有返回值(特征2),上述代码运行的结果如下:

image-20221027195228167

显然我们并没有初始化d1,但是输出的d1结果是已经被初始化完成的,所以在实例化d1的时候就已经自动调用了构造函数。(特征3)

class Date
{public:
	Date(int year, int month = 1, int day = 1)//构造函数
	{_year = year;
		_month = month;
		_day = day;
	}
	Date()//构造函数重载
	{_year = 1970;
		_month = 1;
		_day = 1;
	}
	void Print()
	{cout<< _year<< '/'<< _month<< '/'<< _day<< endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{Date d1;
	Date d2(2022);
	d1.Print();
	d2.Print();
	return 0;
}

在上述代码构造函数的参数中可以看到我们给出了缺省值,调用的时候程序可以正常运行,可见构造函数可以给出缺省值,并且两个构造函数无参和带参构成函数重载(特征4)

这里注意一下,无参和带参全缺省的构造函数不能同时出现,虽然语法上没有问题,但是在调用的时候会产生二义性,出现调用不明确的问题。

在类中如果需要自己实现构造函数的话,一般推荐实现一个全缺省的构造函数即可,防止出现冗余

image-20221027204831075

按照上述的特性5,如果在类中我们没有实现构造函数,那么编译器会自动给我们生成一个无参的构造函数,但是上图中可以看到,似乎编译器自动生成的构造函数并没有什么用,原因在特性6已经说明:编译器自动生成的构造函数并不能对内置类型进行操作,对于自定义的类型,会调用它的默认构造函数。

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

image-20221027205526243

无参构造函数、全缺省构造函数、我们没写编译器默认生成的构造函数,都可以认为是默认构造函数,由于默认构造函数时无参的,所以如果出现多于一个的情况,那么调用的时候就会出现二义性,因此,只能存在一个默认构造函数**(特性7)**。

3.析构函数 3.1概念与特征

上面我们介绍了构造函数,它是对标C语言实现的栈中的StackInit,析构函数对标的就是StackDestory。那么相对应的,析构函数的任务不是销毁对象,而是完成对象中的资源清理工作。

析构函数的特征如下:

  1. 析构函数名是在类名前加上字符 ~。
  2. 无参数无返回值类型。
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。(析构函数不能重载)
  4. 对象生命周期结束时,C++编译器自动调用析构函数。
  5. 编译器生成的默认析构函数,对自定类型成员调用它的析构函数,对内置类型不做操作
  6. 如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
3.2特征分析

下面我们以Stack类为例:

class Stack
{public:
	Stack(int capacity = 4)
	{_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{	perror("malloc fail");
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
  	void Push(int x)
	{//...
	}
private:
	int* _a;
	int _top;
	int _capacity;
};

在定义析构函数的时候,函数名函数名是在类名前加上字符 ~(特征1),并且没有参数和返回值(特征2)。

基于上述的Stack类,我们定义一个MyQueue类:

class MyQueue
{public:
	void Push(int x)
	{_pushST.Push(x);
	}
private:
	Stack _pushST;
	Stack _popST;
};

image-20221107205835813

运行上述代码,发现结果中输出了构造函数和析构函数的函数名,证明在代码运行的过程中自动调用了构造函数和析构函数(特征4)

image-20221107210421467

运行上述代码,我们可以看到调用了两次Stack的构造函数和析构函数,所以对于MyQueue中的Stack的成员,编译器会自动调用他的析构函数(特性5)

4.拷贝构造函数 4.1概念与特征

在定义内置类型的时候,我们有时候会使用类似int a = b这样的语句,这就是一种拷贝,对于自定义类型,我们可以直接拷贝,这种拷贝我们把它叫做浅拷贝,还有一种拷贝叫做深拷贝,深拷贝就是创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”。

我们在实例化一个新的对象的时候,经常可能会遇到这种我们想拷贝已有对象的数据,这时候会用到深拷贝,所以引入了拷贝构造函数的概念。

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

拷贝构造的特征:

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

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

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

  4. 编译器自动生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的

    总结:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

  5. 拷贝构造函数典型调用场景:

    • 使用已存在对象创建新对象

    • 函数参数类型为类类型对象

    • 函数返回值类型为类类型对象

4.2特征分析

以date类为例

class date
{public:
	void print()
	{cout<< _year<< '/'<< _month<< '/'<< _day<< endl;
	}
	date(int year = 1970, 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;
};

拷贝构造函数是构造函数的一个重载形式,拷贝构造的函数参数与构造函数不同**(特性1),由于我们对被拷贝的对象不需要改变它的值,为了安全方面考虑,在参数列表中加上const。拷贝构造的参数类型是类引用,否则就会引发无穷递归调用。(特性2)**

image-20221024163711721

5.运算符重载 5.1运算符重载的概念

在C++中,我们会定义很多个类并且实例化对象,然而我们自定义的这些类对于编译器是陌生的,所以一些操作符对于自定义的类型无法识别,为了增强代码的可读性,C++引入了运算符重载的概念。

运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名为:关键字operator后面接需要重载的运算符符号,例如需要重载+=,那么函数名就是"operator+=",

函数原型:**返回值类型 operator操作符(参数列表) **

注意

  • 不能通过连接其他符号来创建新的操作符:比如operator@

  • 重载操作符必须有一个类类型参数

  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义

  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this

  • .*::sizeof?:.注意以上5个运算符不能重载。

以重载日期类的日期+天数为例:

Date类

class Date
{public:
	Date(int year = 1970, int month = 1, int day = 1)
	{_year = year;
		_month = month;
		_day = day;
	}
	//获取每个月的天数
	int GetMonthDay(int year, int month)
	{static int day[13] = {0,31,28,31,30,31,30,31,31,30,31,30,31 };
		if ((month == 2) && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) //闰年
			return 29;
		return day[month];
	}
	//打印
	void Print()
	{cout<< _year<< "/"<< _month<< "/"<< _day<< endl;
	}

private:
	int _year;
	int _month;
	int _day;
};

重载:

void operator+=(Date& d, int day)
{d._day += day;
	while (d._day >GetMonthDay(d._year, d._month))
	{d._day -= GetMonthDay(d._year, d._month);
		d._month++;

		if (d._month >12)
		{	d._month -= 12;
			d._year++;
		}
	}
}

但是,在我们上手去写的时候就会发现,我们在类外面写运算符重载的话,不能直接访问类里面的private成员变量,所以我们推荐把运算符重载写在类里面,由于类里面的成员函数的第一个参数都是隐藏的this指针,所以写在类里面的运算符重载的参数通常会比运算符少一个操作数,所以在类里面的日期+天数重载代码为:

//运算符重载+=
void operator+=(int day)  //只传递右操作数,通过this操作左操作数
{this->_day += day;  //这里的this->编译器会自动添加
    while (_day >GetMonthDay(_year, _month))
    {_day -= GetMonthDay(_year, _month);
        _month++;

        if (_month >12)
        {_month -= 12;
            _year++;
        }
    }
}
5.2赋值运算符重载

赋值重载既是默认成员函数,又是运算符重载。

5.2.1特性
  1. 赋值重载的格式规范;
  2. 赋值运算符只能重载成类的成员函数不能重载成全局函数;
  3. 若未显式定义,编译器会生成默认的赋值重载函数;
  4. 默认的赋值重载函数对内置类型以字节为单位直接进行拷贝 – 浅拷贝,对自定义类型调用其自身的赋值重载函数;
5.2.2特性分析

1.函数格式:

赋值重载函数的格式一般有如下要求:

使用引用做参数,并以 const 修饰

我们知道,使用传值传参时函数形参是实参的一份临时拷贝,所以传值传参会调用拷贝构造函数;而使用引用做参数时,形参是实参的别名,从而减少了调用拷贝构造在时间和空间上的消耗;另外,赋值重载只会改变被赋值对象,而不会改变赋值对象,所以我们使用 const 来防止函数内部的误操作;

void operator=(const Date& d);

使用引用做返回值且返回值为*this

我们可以对内置类型进行连续赋值,比如int i,j; i = j = 0;那么对于自定义类型来说,我们也可以使用运算符重载来让其支持连续赋值,则重载函数就必须具有返回值;同时,由于我们是在函数外部调用重载函数,所以重载函数调用结束后该对象仍然存在,那么我们就可以使用引用作为函数的返回值,从而减少一次返回值的拷贝,提高程序效率;

另外,我们一般使用左操作数作为函数的返回值,也就是 this 指针指向的对象;

Date& operator=(const Date& d);

检测是否自己给自己赋值

用户在调用成员函数时有可能发生下面这种情况:Date d1; Date& d2 = d1; d1 = d2;这种情况对于只需要浅拷贝的对象来说并没有什么大碍,但对于有资源申请,需要进行深拷贝的对象来说就会发生不可控的事情,具体案例我们在后文中讲解;

在 《Effective C++》中对赋值重载函数自我赋值的解释是这样的:
12227f6e1c1d88df383ecadc57e60e4c

2.重载为成员函数:

赋值运算符只能重载成类的成员函数不能重载成全局函数,这是因为赋值重载函数作为六个默认成员函数之一,如果我们不显示实现,编译器会默认生成;此时用户如果再在类外自己实现一个全局的赋值运算符重载,就会和编译器在类中生成的默认赋值运算符重载冲突,从而造成链接错误。

3.深浅拷贝:

赋值重载函数的特性和拷贝构造函数非常类似 – 如果我们没有显式定义赋值重载,则编译器会自动生成一个赋值重载,且自动生成的函数对内置类型以字节为单位直接进行拷贝,对自定义类型会去调用其自身的赋值重载函数;所以对于没有资源申请的类来说,我们不用自己去写赋值重载函数,直接使用默认生成的即可,因为这种类只需要进行浅拷贝 (值拷贝),比如 Date 类;而对于有资源申请的类来说,我们必须自己手动实现赋值重载函数,来完成深拷贝工作;比如 Stack 类;

**注:**拷贝构造函数完成的是初始化工作,在创建对象时自动调用;赋值重载完成的是已存在的对象之间的拷贝,需要手动调用;

**总结:**自动生成的赋值重载函数对成员变量的处理规则和析构函数一样 – 对内置类型以字节方式按值拷贝,对自定义类型调用其自身的赋值重载函数;我们可以理解为:需要写析构函数的类就需要写赋值重载函数,不需要写析构函数的类就不需要写赋值重载函数;

6.const成员&&取地址及const取地址操作符重载 6.1const成员

我们看下面一个例子:

class date
{public:
	void print()
	{cout<< _year<< '/'<< _month<< '/'<< _day<< endl;
	}
    date(int year = 1970, 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(2022,12,1);
    const date d2(d1);
    d1.print();
    d2.print();
    return 0;
}

这段代码运行会报以下错误,原因是我们在调用d2.print()的时候默认的this指针参数的了类型是date* const this,this指向的值是可修改的,但是实际上d2是const修饰的对象,所以会造成权限的放大。

image-20221221104655319

要解决这类问题的话,我们就需要把this的类型改为const date* const this,但是this指针是隐藏的,所以C++提供了一种新的方式,就是在函数参数的括号后面加const表示修饰*this的const。

void print() const

image-20221221105347175

6.2取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义 ,编译器默认会生成。如果一定要自己定义的话,那么代码是这样的:

class date
{public:
	date* operator&()
	{return this;
	}
	const date* operator&() const
	{return this;
	}
private:
	int _year;
	int _month;
	int _day;
};

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

7、总结

C++的类里面存在六个默认成员函数 – 构造、析构、拷贝构造、赋值重载、取地址重载、const 取地址重载,其中前面四个函数非常重要,也非常复杂,需要我们根据具体情况判断是否需要显式定义,而最后两个函数通常不需要显示定义,使用编译器默认生成的即可;

1、构造函数

  • 构造函数完成对象的初始化工作,由编译器在实例化对象时自动调用;
  • 默认构造函数是指不需要传递参数的构造函数,一共有三种 – 编译器自动生成的、显式定义且无参数的、显式定义且全缺省的;
    如果用户显式定义了构造函数,那么编译器会根据构造函数的内容进行初始化,如果用户没有显式定义,那么编译器会调用默生成的构造函数;
  • 默认生成的构造函数对内置类型不处理,对自定义类型会去调用自定义类型的默认构造;
  • 为了弥补构造函数对内置类型不处理的缺陷,C++11打了一个补丁 – 允许在成员变量声明的地方给缺省值;如果构造函数没有对该变量进行初始化,则该变量会被初始化为缺省值;
  • 构造函数还存在一个初始化列表,初始化列表的存在有着非常大的意义;

2、析构函数

  • 析构函数完成对象中资源的清理工作,由编译器在销毁对象时自动调用;
  • 如果用户显式定义了析构函数,编译器会根据析构函数的内容进行析构;如果用户没有显示定义,编译器会调用默认生成的析构函数;
  • 默认生成的析构函数对内置类型不处理,对自定义类型会去调用自定义类型的析构函数;
  • 如果类中有资源的申请,比如动态开辟空间、打开文件,那么需要我们显式定义析构函数;

3、拷贝构造

  • 拷贝构造函数是用一个已存在的对象去初始化另一个正在实例化的对象,由编译器在实例化对象时自动调用;
  • 拷贝构造的参数必须为引用类型,否则编译器报错 – 值传递会引发拷贝构造函数的无穷递归;
  • 如果用户显式定义了拷贝构造函数,编译器会根据拷贝构造函数的内容进行拷贝;如果用户没有显示定义,编译器会调用默认生成的拷贝构造函数;
  • 默认生成的拷贝构造函数对于内置类型完成值拷贝 (浅拷贝),对于自定义类型会去调用自定义类型的拷贝构造函数;
  • 当类里面有空间的动态开辟时,直接进行值拷贝会让两个指针指向同一块动态内存,从而使得对象销毁时对同一块空间析构两次;所以这种情况下我们需要自己显式定义拷贝构造函数完成深拷贝;

4、运算符重载

  • 运算符重载是C++为了增强代码的可读性而引入的语法,它只能对自定义类型使用,其函数名为 operator 关键字加相关运算符;
  • 由于运算符重载函数通常都要访问类的成员变量,所以我们一般将其定义为类的成员函数;同时,因为类的成员函数的一个参数为隐藏的 this 指针,所以其看起来会少一个参数;
  • 同一运算符的重载函数之间也可以构成函数重载,比如 operator++ 与 operator++(int);

5、赋值重载

  • 赋值重载函数是将一个已存在对象中的数据赋值给另一个已存在的对象,注意不是初始化,需要自己显示调用;它属于运算符重载的一种;
  • 如果用户显式定义了赋值重载函数,编译器会根据赋值重载函数的内容进行赋值;如果用户没有显示定义,编译器会调用默认生成的赋值重载函数;
  • 默认生成的赋值重载函数对于内置类型完成值拷贝 (浅拷贝),对于自定义类型会去调用自定义类型的赋值重载函数;
  • 赋值重载函数和拷贝构造函数一样,也存在着深浅拷贝的问题,且其与拷贝构造函数不同的地方在于它还很有可能造成内存泄漏;所以当类中有空间的动态开辟时我们需要自己显式定义赋值重载函数来释放原空间以及完成深拷贝;
  • 为了提高函数效率与保护对象,通常使用引用作参数,并加以 const 修饰;同时为了满足连续赋值,通常使用引用作返回值,且一般返回左操作数,即 *this;
  • 赋值重载函数必须定义为类的成员函数,否则编译器默认生成的赋值重载会与类外自定义的赋值重载冲突;

6、const 成员函数

  • 由于指针和引用传递参数时存在权限的扩大、缩小与平移的问题,所以 const 类型的对象不能调用成员函数,因为成员函数的 this 指针默认是非 const 的,二者之间传参存在权限扩大的问题;
  • 同时我们为了提高函数效率以及保护对象,一般都会将成员函数的第二个参数使用 const 修饰,这就导致了该对象在成员函数内也不能调用其他成员函数;
  • 为了解决这个问题,C++设计出了 const 成员函数 – 在函数最后面添加 const 修饰,该 const 只修饰 this 指针,不修饰函数的其他参数;
  • 所以如果我们在设计类时,只要成员函数不改变第一个对象,我们建议最后都使用 const 修饰;

7、取地址重载与 const 取地址重载

  • 取地址重载与 const 取地址重载是获取一个对象/一个只读对象的地址,需要自己显式调用;它们属于运算符重载,同时它们二者之间还构成函数重载;
  • 大多数情况下我们都不会去显示实现这两个函数,使用编译器默认生成的即可;只有极少数情况需要我们自己定义,比如防止用户获取到一个对象的地址;
    员函数,否则编译器默认生成的赋值重载会与类外自定义的赋值重载冲突;

6、const 成员函数

  • 由于指针和引用传递参数时存在权限的扩大、缩小与平移的问题,所以 const 类型的对象不能调用成员函数,因为成员函数的 this 指针默认是非 const 的,二者之间传参存在权限扩大的问题;
  • 同时我们为了提高函数效率以及保护对象,一般都会将成员函数的第二个参数使用 const 修饰,这就导致了该对象在成员函数内也不能调用其他成员函数;
  • 为了解决这个问题,C++设计出了 const 成员函数 – 在函数最后面添加 const 修饰,该 const 只修饰 this 指针,不修饰函数的其他参数;
  • 所以如果我们在设计类时,只要成员函数不改变第一个对象,我们建议最后都使用 const 修饰;

7、取地址重载与 const 取地址重载

  • 取地址重载与 const 取地址重载是获取一个对象/一个只读对象的地址,需要自己显式调用;它们属于运算符重载,同时它们二者之间还构成函数重载;
  • 大多数情况下我们都不会去显示实现这两个函数,使用编译器默认生成的即可;只有极少数情况需要我们自己定义,比如防止用户获取到一个对象的地址;

参考野猪佩奇大佬博客:大佬博客链接

你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧


文章题目:【C++】类和对象(中)-创新互联
分享路径:http://www.cdkjz.cn/article/despep.html
多年建站经验

多一份参考,总有益处

联系快上网,免费获得专属《策划方案》及报价

咨询相关问题或预约面谈,可以通过以下方式与我们联系

大客户专线   成都:13518219792   座机:028-86922220