C++ 再识类和对象

目录
  • 类的6个默认成员函数
  • 构造函数
    • 1.概念
    • 2.特性
    • 隐式构造函数
      • 无参和全缺省的函数均为默认构造函数
    • 成员变量的命名风格
      • 补充
  • 析构函数
    • 1.概念
    • 2.特性
      • c++编译器在对象生命周期结束时自动调用析构函数
  • 拷贝构造函数
    • 1.概念
    • 2.特性
      • 若未显式定义,系统会生成默认的拷贝构造函数
      • 浅拷贝的注意事项
  • 总结

类的6个默认成员函数

一个类中如果什么成员都没有,那么这个类称为空类。空类中是什么都没有吗?其实不然,任何一个类,再我们不写的情况下,都会自动生成下面6个默认成员函数:

本篇文章将对这几个默认成员函数进行简单介绍。

构造函数

1.概念

我们先来看一下下面这个日期类:

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

对于Date类,每次创建对象时可以调用SetData函数来设置对象的日期,但是如果每次创建对象时都需要调用该函数来设置日期信息,未免有些麻烦,那么能否再对象创建的同时就进行初始化呢?

这里就需要用到类的默认成员函数–构造函数了。

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

2.特性

需要注意,构造函数虽然名为构造函数,但是其作用并非为成员变量开辟空间,而是初始化对象。其特征如下:

函数名与类名相同。

没有返回值。

编译器会再对象实例化时自动调用构造函数。

构造函数可以重载。

需要注意的是在类实例化对象的时候,如果变量后面带上了(),而括号内没有参数,那么这就成了函数声明,该函数无参,且返回值为类名。

class Date
{
public:
	Date()//无参的构造函数
	{
		_year = 0;
		_month = 1;
		_day = 1;
	}
	//带参的构造函数
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;//调用无参的构造函数
	Date d2(0, 1, 1);//调用带参的构造函数
	Date d3();//无参,返回值为Date的函数声明
	return 0;
}

隐式构造函数

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

需要注意的是编译器自己生成的构造函数在初始化对象时做了一个偏心的处理:即对于内置类型,编译器不会处理;而对于自定义类型,编译器会自定义类型调用它自己的默认构造函数。内置类型指的是语法已经定义好的类型,如:int,double,long等等;自定义类型是使用struct/class/union定义的类型。

这是什么意思呢?我们通过下面这个代码来理解:

class C
{
public:
	C()
	{
		cout << "C()" << endl;
	}
private:
	int _c;
};
class Date
{
public:
	//若用户显式定义了构造函数,那么编译器将不再生成
	/*Date()
	{
		_year = 0;
		_month = 1;
		_day = 1;
	}
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}*/
private:
	//内置类型
	int _year;
	int _month;
	int _day;
	//自定义类型
	C c1;
};
int main()
{
	Date d1;//调用无参的构造函数
	return 0;
}

通过调试可以发现,d1自身的内置类型变量仍为随机值,编译器调用的构造函数并没有处理,而对于自定义类型,可以看到编译器调用了自定义类型中的默认函数,但是实际上如果调用编译器自己生成的默认构造函数,最终的结果就是所有的内置类型变量仍然为随机值,这么看下来好像编译器自己生成的构造函数好像没什么用?

实则不然,比如我们曾做过用栈实现队列的题,这道题的思路是用两个栈来回倒保证队列的先进先出,而这里面的两个结构栈和用栈实现的队列的代码为:

class Stack//栈
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
struct MyQueue//用两个栈实现队列
{
	Stack s1;
	Stack s2;
};

可以看到在用MyQueue这个类实例化对象时,编译器调用Stack中的构造函数分别对成员变量s1和s2初始化,因此,我们无需再对其进行初始化了,这相对来说方便了许多。

无参和全缺省的函数均为默认构造函数

无参的构造函数和全缺省的构造函数都被称为默认构造函数,但是需要注意的是:无参的构造函数和全缺省的构造函数二者只能存在一个,这是因为,如果二者都存在的话,那么在实例化对象不带参数时,编译器无法区分是调用哪一个函数。

class Date
{
public:
	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;
};
int main()
{
	Date d1;//错误,编译器无法识别要调用哪一个构造函数
	return 0;
}

在实际过程中,我们更倾向于使用全缺省的构造函数,因为它包含了无参的构造函数的情况。

成员变量的命名风格

可以注意到的是在定义类的时候成员变量前都加了一个_,这是为了防止下面这种情况:

class Date
{
public:
	Date(int year = 0, 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;
}

可以看到,d1调用了构造函数后,其成员变量认为随机值。这是因为在year = year这句代码中,两个year变量均为函数形参,实际上编译器在处理这种变量时,会遵循局部优先原则,即编译器在函数形参中找到了year变量,就不会继续扩大搜索范围去寻找成员变量中的year变量,而在Print函数中,编译器由于在形参中未找到year变量,因此继续扩大搜索范围,在成员变量中找到了year并使用之。

因此,在声明成员变量的命名时需要遵循一定的规范,常见的有:(1)在变量名前加_,如_year (2)在变量名后加_,如year_ (3)驼峰法,如mYear,m表示member。

另外,上述情况可以通过使用this指针进行解决,即将代码改为this->year = year;但在实际使用过程中,最好还是注重成员变量的命名

补充

由于早期c++语法设计的缺陷,编译器默认生成的构造函数并不会对内置类型变量初始化,因此在c++11后,语法委员会在成员变量声明处打了一个补丁,运行,变量声明的同时加上缺省值,比如:

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	//注意,此处仅为缺省值,仍为变量声明,而非初始化(定义)
	int _year = 0;
	int _month = 1;
	int _day = 1;
};

析构函数

1.概念

与构造函数相比,析构函数相对简单一些。析构函数的作用与构造函数的相反,析构函数并不是完成对象的销毁,因为局部对象的销毁工作是由编译器来完成的。一个词来概括析构函数的作用就是清理,即对象在销毁的时候会自动调用析构函数,完成类当中的一些资源清理工作。

2.特性

析构函数是一种特殊的成员函数,其特征如下:

析构函数名是类名前加上~号

析构函数无参数无返回值

一个类有且只有一个析构函数

若析构函数为显式定义,那么系统会自动生成默认的析构函数。

与构造函数一样,系统的默认析构函数对于内置类型变量不会处理,对于自定义变量会调用其自身的析构函数。

其次,对于Date类这样的类,由于其内部没有什么资源需要处理,因此不需要析构函数;对于Stack这样的类,其内部由资源需要处理,比如对malloc出来的空间进行释放,因此需要实现析构函数。

还是之前的代码,在用两个栈实现队列中,在Stack类中实现了构造函数和析构函数,那么用MyQueue实例化my变量后无法自己实现初始化和空间的释放:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		free(_a);
		_a = NULL;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
struct MyQueue
{
	Stack s1;
	Stack s2;
};
int main()
{
    //我们无需自己对mq进行初始化和清理空间
    //编译会自动调用构造函数和析构函数
	MyQueue mq;
	return 0;
}

c++编译器在对象生命周期结束时自动调用析构函数

class Date
{
public:
	Date(int year = 0, int month = 1, int day = 1)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	~Date()
	{
		cout << "~Date()" << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	return 0;//编译器在执行这句代码的同时会调用类中的析构函数
}

拷贝构造函数

1.概念

拷贝构造函数,顾名思义,其作用就是创建一个和被拷贝对象一模一样的对象。

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

2.特性

拷贝构造函数也是特殊的成员函数,其特征是:

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

参数只有一个且为引用传参

拷贝构造函数的参数只有一个且必须为引用传参,使用传值方式会引发无穷递归调用。

class Date
{
public:
	Date()
	{
		_year = 0;
		_month = 1;
		_day = 1;
	}
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

那么为什么说传值会导致无穷递归调用呢?首先我们需要理解到调用函数传值给形参也是一种拷贝,比如说:

同样的,对于拷贝构造函数,若形参为传值调用,那么在上述代码中将d2赋值给形参d时也会调用拷贝构造函数,而每一次调用拷贝构造函数都会经过依次赋值操作,从而导致无穷递归调用:

而传引用就能够很好的解决这个问题,其次,传指针也可以达到目的,不过一般传引用的话可以增强代码可读性。

若未显式定义,系统会生成默认的拷贝构造函数

与构造函数一样,如果我们自己没有实现拷贝构造函数,那么编译器会生成默认的拷贝构造函数;但是与构造函数不同的是,默认的拷贝构造函数对于内置类型和自定义类型变量都会处理:

(1)对于内置类型,默认的拷贝构造函数会对对象进行浅拷贝,即按照内存存储中的字节序对对象进行拷贝,也叫值拷贝。

(2)对于自定义类型,默认的拷贝构造函数会调用自定义类型中自己的拷贝构造函数。

class A
{
public:
	A()
	{
		_a = 0;
	}
	A(const A& a)
	{
		cout << "A(const A& a)" << endl;
	}
private:
	int _a;
};
class Date
{
public:
	Date()
	{
		_year = 0;
		_month = 1;
		_day = 1;
	}
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	//调用默认的拷贝构造函数
	/*Date(Date& d)
	{
		_year = d._year;
		_month = d._month;
		_day = d._day;
	}*/
private:
	int _year;
	int _month;
	int _day;
	A aa;
};
int main()
{
	Date d1;
	Date d2(d1);
	return 0;
}

浅拷贝的注意事项

通过上面我们知道了默认的拷贝构造函数能够实现浅拷贝,也就是说,对于Date这样的类,我们无需自己实现拷贝构造函数只用默认的拷贝构造函数就能够实现拷贝目的,那么是否用编译器自己的函数就够了呢?

其实不然,比如我们熟知的Stack类,如果直接调用系统默认的拷贝构造函数:

class Stack
{
public:
	Stack(int capacity = 4)
	{
		_a = (int*)malloc(sizeof(int) * capacity);
		if (_a == nullptr)
		{
			cout << "malloc fail" << endl;
			exit(-1);
		}
		_top = 0;
		_capacity = capacity;
	}
	~Stack()
	{
		free(_a);
		_a = NULL;
		_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
int main()
{
	Stack s1(8);
	Stack s2(s1);
	return 0;
}

上述代码,我们运行后发现,程序崩溃了,这是为什么呢?这是因为系统默认的拷贝构造函数拷贝出了一份与s1一模一样的s2:

而我们知道当对象的生命周期结束时,系统会自动调用析构函数对类空间进行清理,由于s2是后压栈的,因此会先清理,这时s2._a所指的空间已经free还给操作系统了,但是s1还会再次调用析构函数,将已经释放的s1._a所指向的空间再一次释放(注意,s2._a释放完后s1._a仍指向原空间,此时s1._a为野指针),这个操作最终会导致程序崩溃。

可见编译器默认的拷贝构造函数并不能解决所有的问题,浅拷贝会导致一些错误,那么要如何解决浅拷贝的带来的问题呢?这就要我们之后介绍的深拷贝来解决了。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • C++初识类和对象

    目录 一.初步认识面向过程和面向对象 二.类的引入 三.类的定义 1.定义和声明全部放在类体中,需要注意的是: 2.声明与定义分离 四.类的访问限定符及封装 1.访问限定符 2.封装 五.类的作用域 六.类的实例化 七.类对象模型 1.计算类对象的大小 2.类对象的存储方式 八.this指针 1.this指针的引出 2.this指针的特性 总结 一.初步认识面向过程和面向对象 面向过程,关注的是怎么去做,比如在外卖系统中,强调点餐,做餐,送餐等一系列动作的方法,反映到语言中是函数方法的实现:而面

  • C++入门浅谈之类和对象

    目录 一.面向过程vs面向对象 二.类的限定符及封装 三.类的实例化 四.this指针 五.默认成员函数 1. 构造函数 2. 析构函数 3. 拷贝函数 4. 赋值运算符重载 总结 一.面向过程vs面向对象 C语言面向过程,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题 C++是基于面向对象,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成,C++不是纯面向对象的语言,C++既有面向过程,也有面向对象可以混合编程.C语言面向过程,数据和方法是分离的.CPP面向对象,数

  • C++类和对象补充

    目录 一. 再看构造函数 1.函数体内赋初值 2.初始化列表 几点注意 3.explicit关键字 二.static成员 1.概念 2.特性 三.友元 1.友元函数 2.友元类 四.内部类 总结 一. 再看构造函数 我们之前已经了解了构造函数的基本内容,那么这里我们将深入认识构造函数. 1.函数体内赋初值 class Date { public: Date(int year, int month, int day) { _year = year; _month = month; _day = d

  • C++进一步认识类与对象

    目录 赋值操作符重载函数 1.运算符重载 2.赋值运算符重载 3.默认的赋值操作符重载函数 4.赋值重载函数与拷贝构造函数的对比 日期类的实现 const成员 1.const修饰类的成员函数 2.小结 取地址及const取地址操作符重载函数 总结 赋值操作符重载函数 1.运算符重载 C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似. 其函数名为: operator + 需要重载的运算符符

  • C++类与对象之运算符重载详解

    目录 运算符重载 加号运算符重载 左移运算符重载 递增运算符重载 递减运算符重载 赋值运算符重载 关系运算符重载 函数调用运算符重载 总结 运算符重载 运算符重载概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型 加号运算符重载 作用:实现两个自定义数据类型相加的运算 #include <iostream> using namespace std; class Person { public: // 构造函数 Person(int num1, int num2){ thi

  • C++ 再识类和对象

    目录 类的6个默认成员函数 构造函数 1.概念 2.特性 隐式构造函数 无参和全缺省的函数均为默认构造函数 成员变量的命名风格 补充 析构函数 1.概念 2.特性 c++编译器在对象生命周期结束时自动调用析构函数 拷贝构造函数 1.概念 2.特性 若未显式定义,系统会生成默认的拷贝构造函数 浅拷贝的注意事项 总结 类的6个默认成员函数 一个类中如果什么成员都没有,那么这个类称为空类.空类中是什么都没有吗?其实不然,任何一个类,再我们不写的情况下,都会自动生成下面6个默认成员函数: 本篇文章将对这

  • Python面向对象编程中的类和对象学习教程

    Python中一切都是对象.类提供了创建新类型对象的机制.这篇教程中,我们不谈类和面向对象的基本知识,而专注在更好地理解Python面向对象编程上.假设我们使用新风格的python类,它们继承自object父类. 定义类 class 语句可以定义一系列的属性.变量.方法,他们被该类的实例对象所共享.下面给出一个简单类定义: class Account(object): num_accounts = 0 def __init__(self, name, balance): self.name =

  • 关于JavaScript定义类和对象的几种方式

    可以看看这个例子: 复制代码 代码如下: var a = 'global'; (function () { alert(a); var a = 'local'; })(); 大家第一眼看到这个例子觉得输出结果是什么?'global'?还是'local'?其实都不是,输出的是undefined,不用迷惑,我的题外话就是为了讲这个东西的. 其实很简单,看一看JavaScript运行机制就会明白.我们可以把这种现象看做"预声明".但是如果稍微深究一下,会明白得更透彻. 这里其实涉及到对象属性

  • JavaScript 里的类数组对象

    很早以前我就知道可以把 arguments 转化为数组:[].slice.call(arguments),因为 arguments 是个类数组对象,所以才可以这么用.但是我一直不清楚什么叫做类数组对象( array-like objects) 今天看 Effective JavaScript 就有一节是专门讲这个的,感觉真是太拽了. 先看我写的一些示例代码: 复制代码 代码如下: a = "hello" [].map.call(a, (e) -> e.toUpperCase())

  • Java多态和实现接口的类的对象赋值给接口引用的方法(推荐)

    接口的灵活性就在于"规定一个类必须做什么,而不管你如何做". 我们可以定义一个接口类型的引用变量来引用实现接口的类的实例,当这个引用调用方法时,它会根据实际引用的类的实例来判断具体调用哪个方法,这和上述的超类对象引用访问子类对象的机制相似. //定义接口InterA interface InterA { void fun(); } //实现接口InterA的类B class B implements InterA { public void fun() { System.out.pri

  • 全面理解Java类和对象

    面向对象的程序是由对象组成的,每个对象包含对用户公开的特定功能部分和隐藏的实现部分.在面向对象程序设计(OOP)中,不必关心对象的具体实现.在传统的结构化程序设计中,算法是第一位的,数据结构是第二位的,即首先确定如何操作数,再考虑如何组织数据,以方便操作.而OOP则颠倒了这种次序,将数据放在第一位,然后再考虑操作数据的算法. 一.类 类是构造对象的模板和蓝图.通俗地说,类相当于建筑的图纸,而对象相当于建筑物.由类构造对象的过程称为创建对象的实例. Java中通过关键字class定义"类"

  • 解析Java的JVM以及类与对象的概念

    Java虚拟机(JVM)以及跨平台原理 相信大家已经了解到Java具有跨平台的特性,可以"一次编译,到处运行",在Windows下编写的程序,无需任何修改就可以在Linux下运行,这是C和C++很难做到的. 那么,跨平台是怎样实现的呢?这就要谈及Java虚拟机(Java Virtual Machine,简称 JVM). JVM也是一个软件,不同的平台有不同的版本.我们编写的Java源码,编译后会生成一种 .class 文件,称为字节码文件.Java虚拟机就是负责将字节码文件翻译成特定平

  • 一篇文章搞懂Python的类与对象名称空间

    代码块的分类 python中分几种代码块类型,它们都有自己的作用域,或者说名称空间: 文件或模块整体是一个代码块,名称空间为全局范围 函数代码块,名称空间为函数自身范围,是本地作用域,在全局范围的内层 函数内部可嵌套函数,嵌套函数有更内一层的名称空间 类代码块,名称空间为类自身 类中可定义函数,类中的函数有自己的名称空间,在类的内层 类的实例对象有自己的名称空间,和类的名称空间独立 类可继承父类,可以链接至父类名称空间 正是这一层层隔离又连接的名称空间将变量.类.对象.函数等等都组织起来,使得它

  • c++ 类和对象总结

    话不多说,我们直接进入主题: 对象:客观世界里的一切事物都可以看作是一个对象,每一个对象应当具有属性(静态特征,比如一个班级,一个专业,一个教室)和行为(动态特征,例如:学习,开会,体育比赛等)两个要素. 对象是由一组属性和一组行为构成的. 类(class):就是对象的类型,代表了某一批对象的共同特性和特征.类是对象的抽象,而对象是类的具体实例. 2.1 类的引入 在C语言中我们定义一个结构体是这样定义的: struct Student { int _age; char* _Gender; ch

  • 简述 Python 的类和对象

    系列最后一篇来说说Python中的类与对象,Python这门语言是无处不对象,如果你曾浅要了解过Python,你应该听过Python是一种面向对象编程的语言,所以你经常可能会看到面向"对象"编程这类段子,而面向对象编程的语言都会有三大特征:封装.继承.多态. 我们平时接触到的很多函数.方法的操作都具有这些性质,我们只是会用,但还没有去深入了解它的本质,下面就介绍一下关于类和对象的相关知识. 封装 封装这个概念应该并不陌生,比如我们把一些数据封装成一个列表,这就属于数据封装,我们也可以将

随机推荐