C++超详细讲解构造函数

目录
  • 类的6个默认成员函数
  • 构造函数
  • 特性
  • 编译器生成的默认构造函数
  • 成员变量的命名风格

类的6个默认成员函数

如果我们写了一个类,这个类我们只写了成员变量没有定义成员函数,那么这个类中就没有函数了吗?并不是的,在我们定义类时即使我们没有写任何成员函数,编译器会自动生成下面6个默认成员函数。

class S
{
public:
	int _a;
};

这里就来详细介绍一下构造函数。

构造函数

使用C语言,我们用结构体创建一个变量时,变量的内容都是随机值,要想要能正确的操作变量中存储的数据,我们还需要调用对应的初始化函数,给成员变量赋一个合适的初值。那么C++呢,我们仍然使用这个方法来试试。

class Date
{
public:
	void SetDate(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1, d2;
	d1.SetDate(2018, 5, 1);//初始化
	d1.Display();
	d2.SetDate(2018, 7, 1);//初始化
	d2.Display();
	return 0;
}

完全没问题的,毕竟C++完全兼容C嘛,不过这样子做未免有点麻烦,能不能做到我一创建好对象,对象的成员变量就是已经被初始化了而不是我们主动去调用呢?C++提供了一个特殊的函数:构造函数。

构造函数是一个特殊的成员函数,名字与类名相同,无返回值,每次使用类实例化对象时会自动调用,保证每个数据成员都有一个合适的初值,方便我们的后续使用,构造函数在对象的生命周期内只会被调用一次。

特性

虽然叫构造函数,但它的作用并不是构造一个对象(申请空间创建对象),而是初始化对象。

特征如下

  • 函数名与类名相同;
  • 无返回值;
  • 对象实例化时编译器自动调用对应的构造函数;
  • 构造函数可以重载;
class Date
{
public:
	Date()//无参的构造函数
	{

	}
	Date(int year, int month, int day)//带参的构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;//调用无参的构造函数
	Date d2(2001, 7, 28);//调用带参的构造函数
	d1.Display();
	d2.Display();
    Date d3();//这种写法要不得,会被当做函数名为d3,无参、返回类型为Date的函数声明。
    //调用无参的构造函数,一定不要加()否则编译器无法识别这是一个函数声明还是调用的无参构造。
	return 0;
}

如果编译器没有显式的定义构造函数,编译器会自动生成一个无参的默认构造函数,一旦我们显式定义,编译器就不再生成;

class Date
{
public:
	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;//同样能创建一个对象
	d1.Display();
	return 0;
}

输出:

可以看到使用编译器生成的默认构造函数我们的日期仍然是随机值。

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

class Date
{
public:
	Date()//无参的默认构造函数
	{
	}
	Date(int year = 2001, int month = 7, int day = 28)//全缺省的默认构造函数
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year;
	int _month;
	int _day;
};
int main()
{
	Date d1;//这里无法编译通过,因为调用不明确。
	d1.Display();
	return 0;
}

一般我们使用全缺省的构造函数,既可以不传参用缺省值去初始化对象,也可以显式地去调用并用实参初始化对象。

编译器生成的默认构造函数

欸好像这货没什么用啊,我刚刚使用这个自动生成的,我的对象的初值还是随机值啊,看起来就像这构造函数什么事都没有做。

其实C++把类型分成内置类型(基本类型)和自定义类型。

内置类型就是语法已经定义好的类型:如int/char…,自定义类型就是我们使用class/struct/union自己定义的类型,看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。

不过这涉及到我们还没学过的知识——初始化列表,后面会讲。

class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
	{
		cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl;
		_hour = hour;
		_minute = minute;
		_second = second;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
    //使用编译器默认的构造函数
	void Display()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
    //内置类型
	int _year;
	int _month;
	int _day;
    //自定义类型
	Time _t;
};
int main()
{
	Date d1;
	d1.Display();
	return 0;
}

输出:

Time(int hour = 0,int minute = 0,int second = 0)

-858993460–858993460–858993460

事实上编译器生成的默认构造函数并不是什么都没有做,而是只处理了成员变量中的自定义类型,而没有去初始化内置类型,调用自定义类型成员的构造函数就是在初始化列表做的,下面会详细讲。

成员变量的命名风格

看看下面这种成员命名方式有什么缺陷?

class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
	{
		cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl;
		hour = hour;
		minute = minute;
		second = second;
	}
private:
	int hour;
	int minute;
	int second;
};

简直太难看了,hour = hour这是什么操作???到底哪个hour是成员变量,让人去猜吗,代码实在丑陋,因此我们在对成员变量命名时,为了初始化对象不会因为发生命名冲突,又能一眼看出来哪个形参是对应初始化哪个成员变量的,我们通常在对成员变量命名方式统一成成员变量名前加上下划线_。

class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
	{
		cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl;
		_hour = hour;
		_minute = minute;
		_second = second;
	}
private:
	int _hour;
	int _minute;
	int _second;
};

当然不止是这样,适合自己的才是最好的,不过一般都是采用加一个前缀或者加上一个后缀的方式来命名成员变量。

这里很容易弄混两个概念,在此强调一下:

默认构造函数是不需要参数的构造函数,有以下三种:

  • 编译器生成的;
  • 显式定义的无参的构造函数;
  • 显式定义的全缺省的构造函数;

默认成员函数是我们如果不写,编译器会自动生成的函数;

构造函数的初始化列表

前面说了,在实例化一个类的对象时会自动去调用类的构造函数进行对象的初始化操作,那在C++中一个自定义类型的过程可分为两个过程:

  • 为对象分配内存;
  • 对成员变量赋值;
  • 函数体内赋值;

那我们想想如果成员变量是具有常属性的,那么是不是2过程就无法生效了?那么对于那些具有常性的变量我们以前是怎么定义的呢?初始化操作可以完成这个问题。

初始化是什么?变量在定义的同时为它设定一个初值,例如:引用必须初始化

int& a = 10;,这就是一个典型的初始化操作,而我们要谈的初始化列表也是相似的,那么这种初始化操作有什么与众不同的呢?

答案是:对于那些一旦有初值就不能再被赋值的变量,初始化列表的作用就体现出来了,例如被const修饰的变量,或者是引用,这些都是具有常属性的,因此就需要在它们创建的过程中就给它们一个初值,保证其可以被正常初始化。

class S
{
public:
	S(int i = 0, int j = 0)
	{
		_i = i;
		_j = j;
	}
private:
	const int _i;//const修饰i具有常属性
	int _j;
};
int main()
{
	S s;
	return 0;
}

报错:

“S::_i”: 必须初始化常量限定类型的对象

error C2166: 左值指定 const 对象

即在编译器看来,这个对象包括对象中的成员变量在进入构造函数的函数体后,就都已经初始化完成了,因此const修饰的变量i就无法再被赋值了。

既然初始化列表是在创建变量阶段对变量进行的初始化,因此就可以使用初始化列表处理给那些无法修改的变量。

初始化列表格式如下:

class S
{
public:
	S(int i = 0, int j = 0)
		:_i(i),
		_j(j)
	{}
private:
	const int _i;
	int _j;
};
int main()
{
	S s;
	return 0;
}

程序正常运行

同时呢,建议能使用初始化列表就使用,尽量不在构造函数的函数体中为成员变量再赋值,养成好的习惯。

下面分析编译器生成的默认构造函数到底做了什么事情

class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:_hour(hour),
		_minute(minute),
		_second(second)
	{
		cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl;
	}
	void DisPlay()
	{
		cout << _hour << "-" << _minute << "-" << _second << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	void Display()
	{
		cout << "Date:";
		cout << _year << "-" << _month << "-" << _day << endl;
		cout << "Time:";
		_t.DisPlay();
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d1;
	d1.Display();
	return 0;
}

输出:

可以看到,Date类中的内置类型都未被初始化,而对于自定义类型是去调用了其默认构造函数并且初始化成功了的。

这里要记住一定是调用的自定义成员的默认构造函数,因为编译器生成的Date的默认构造函数调用Time的构造时默认是不传参的,毕竟它也不知道传什么嘛。

如果我们把Time构造函数的缺省值去掉,那么Time就没有默认构造函数,那么创建Date对象时就无法调用Time的默认构造函数,就出错了。如下

class Time
{
public:
	Time(int hour, int minute, int second)
		:_hour(hour),
		_minute(minute),
		_second(second)
	{
		cout << "Time(int hour = 1, int minute = 1, int second = 1)" << endl;
	}
	void DisPlay()
	{
		cout << _hour << "-" << _minute << "-" << _second << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	void Display()
	{
		cout << "Date:";
		cout << _year << "-" << _month << "-" << _day << endl;
		cout << "Time:";
		_t.DisPlay();
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d1;
	d1.Display();
	return 0;
}

报错:

message : “Date::Date(void)”: 由于 数据成员“Date::_t”不具备相应的 默认构造函数

那我们现在已经知道了编译器生成的默认构造函数它能做什么了,接下来我们显式地去定义一个构造函数。

class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:_hour(hour),
		_minute(minute),
		_second(second)
	{
		cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl;
	}
	void DisPlay()
	{
		cout << _hour << "-" << _minute << "-" << _second << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date()
		:
		_year(),//调用了int的默认构造函数,并且会给初始化为0
		_month(),
		_day(),
		_t()//调用了Time的默认构造函数,这里不写也会自动调用
	{
		cout << "Date()" << endl;
	}
	void Display()
	{
		cout << "Date:";
		cout << _year << "-" << _month << "-" << _day << endl;
		cout << "Time:";
		_t.DisPlay();
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d1;
	d1.Display();
	return 0;
}

我们为Date定义一个无参的默认构造函数,在初始化列表我不止处理了内置类型还处理了自定义类型。

在C++中支持这样一种定义变量的方法:

	int a;//a是随机值
	int b(1);
	int();//创建匿名变量,调用默认构造

这和自定义类型的定义是一样的格式,这是为了让内置类型也能按照自定义类型的方式去定义和初始化,看起来是去调用了int的构造函数。

这样,我们在初始化列表中调用了初始化了内置类型和自定义类型,Date的内置类型也都被初始化为0了,这也印证了编译器生成的默认构造函数并没有去调用int的默认构造,而只调用了自定义的默认构造。

注意:就算我们显式的定义了构造函数,如果在初始化列表中不显式的调用Time的构造函数,那么编译器也会默认去调用它的默认构造(创建自定义类型成员变量时就一定会调用),而我们一旦显式的去调用了,那么走我们的调用。

C++中有这样一个特性,编译器能帮你做的,就算你不做它会自动帮你完成,而你一旦做了他就会按照你的方式去完成。

具体什么意思呢?看代码:

class Time
{
public:
	Time(int hour = 0, int minute = 0, int second = 0)
		:_hour(hour),
		_minute(minute),
		_second(second)
	{
		cout << "Time(int hour = 0, int minute = 0, int second = 0)" << endl;
	}
	void DisPlay()
	{
		cout << _hour << "-" << _minute << "-" << _second << endl;
	}
private:
	int _hour;
	int _minute;
	int _second;
};
class Date
{
public:
	Date()
		:
	_t()//这里即使不写,编译器会自动去调
	{
		cout << "Date()" << endl;
	}
	void Display()
	{
		cout << "Date:";
		cout << _year << "-" << _month << "-" << _day << endl;
		cout << "Time:";
		_t.DisPlay();
	}
private:
	int _year;
	int _month;
	int _day;
	Time _t;
};
int main()
{
	Date d1;
	d1.Display();
	cout << int() << endl;
	return 0;
}

其实这个Date类的构造函数的初始化过程可以说就是编译器默认生成的构造相同了。

总之就是编译器生成的默认构造函数会在初始化列表中调用成员中自定义类型的默认构造,而不会处理内置类型,不论是我们显式定义的还是编译器生成的,编译器都会默认去调用自定义类的构造函数,除非我们在初始化列表中显式的去调用了成员的构造函数。

再言简意骇,就是无论什么构造函数(自动生成,显式定义)编译器都会在初始化列表调用自定义类型成员的构造函数,而如果我们自己显式调用了成员的构造,就执行我们所写的。

可以不显式定义构造函数的情况

如果成员变量都是自定义类型并且不需要显式调用构造函数,那么编译器生成的默认构造函数就足以处理这种情况

​ 其他情况都需要我们自己去定义构造函数。

到此这篇关于C++超详细讲解构造函数的文章就介绍到这了,更多相关C++构造函数内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C++超详细讲解构造函数与析构函数的用法及实现

    目录 写在前面 构造函数和析构函数 语法 作用 代码实现 两大分类方式 三种调用方式 括号法 显示法 隐式转换法 正确调用拷贝构造函数 正常调用 值传递的方式给函数参数传值 值传递方式返回局部对象 构造函数的调用规则 总结 写在前面 上一节解决了类与对象封装的问题,这一节就是对象的初始化和清理的构造函数与析构函数的内容了:对象的初始化和清理也是两个非常重要的安全问题:一个对象或者变量没有初始状态,对其使用后果是未知,同样的使用完一个对象或变量,没有及时清理,也会造成一定的安全问题:c++利用了构

  • C++的拷贝构造函数你了解吗

    目录 一般情况下的拷贝构造函数: 默认拷贝构造函数: 浅拷贝和深拷贝: 总结 拷贝构造函数用以将一个类的对象拷贝给同一个类的另一个对象,比如之前学习过的string类: string s1; string s2 = s1; 一般情况下的拷贝构造函数: class A { private: int n; double d; char s; public: A(const A& a); }; A::A(const A& a) { this->n = a.n; this->d = a

  • C++中拷贝构造函数的使用

    目录 拷贝构造函数 1. 手动定义的拷贝构造函数 2. 合成的拷贝构造函数 总结 拷贝构造函数 拷贝构造函数,它只有一个参数,参数类型是本类的引用.复制构造函数的参数可以是 const 引用,也可以是非 const 引用. 一般使用前者,这样既能以常量对象(初始化后值不能改变的对象)作为参数,也能以非常量对象作为参数去初始化其他对象.一个类中写两个复制构造函数,一个的参数是 const 引用,另一个的参数是非 const 引用,也是可以的. 1. 手动定义的拷贝构造函数 Human.h #pra

  • c++详细讲解构造函数的拷贝流程

    #include <iostream> #include <string> using namespace std; void func(string str){ cout<<str<<endl; } int main(){ string s1 = "http:www.biancheng.net"; string s2(s1); string s3 = s1; string s4 = s1 + " " + s2; fu

  • C++探索构造函数私有化会产生什么结果

    目录 对于单个类 私有化与继承 成员变量与私有化 提问:假设只有一个构造方法,如果将之私有化会有什么后果 对于当前类,它是无法实例化的 对于它的子类,子类也是无法实例化的 构造函数与是否能够实例化有关 对于单个类 正常情况下 #include <iostream> using namespace std; class EventDispatcher { public: void test_printf(){ std::cout << "test_printf --\r\n

  • C++中构造函数详解

    构造函数按参数为为:有参构造函数和无参构造函数 按类型分为:普通构造函数和拷贝构造函数 构造函数的三种调用方法:括号法,显示法,隐式转换法: //括号法 Person p1; //默认构造 无参构造 Person p2(13); //有参构造 Person p3(p2); //拷贝构造 //注意:使用无参构造时不要写括号.不然系统会认为该语句是函数声明. 例:Person p1(); //显示法 Person p1; Person p2 = Person(13);//有参构造 Person p3

  • C++构造函数的类型,浅拷贝与深拷贝详解

    目录 一.无参构造函数 二.含参构造函数 三.拷贝构造函数 四.深拷贝和浅拷贝 总结 一.无参构造函数 1.如果没有定义构造函数,则系统自动调用此默认构造函数,且什么都不做. 2.如果用户自定义了带参数的构造函数,若还想调用无参的构造函数,必须显示定义 person() { cout << "this object is being created." << endl; } 二.含参构造函数 一般构造函数可以有各种参数形式,一个类可以有多个一般构造函数,前提是参

  • C++超详细讲解拷贝构造函数

    目录 构造函数 特征 编译器生成的拷贝构造 拷贝构造的初始化列表 显式定义拷贝构造的误区 结论 构造函数 只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用 拷贝构造函数是构造函数的一个重载,因此显式的定义了拷贝构造,那么编译器也不再默认生成构造函数. 特征 拷贝构造也是一个特殊的成员函数 特征如下: 拷贝构造是构造函数的一个重载: 拷贝构造的参数只有一个并且类型必须是该类的引用,而不是使用传值调用,否则会无限递归: 若没有显

  • C++中的拷贝构造函数详解

    目录 C++拷贝构造函数(复制构造函数)详解 1) 为什么必须是当前类的引用呢? 2) 为什么是 const 引用呢? 默认拷贝构造函数 总结 C++拷贝构造函数(复制构造函数)详解 拷贝和复制是一个意思,对应的英文单词都是copy.对于计算机来说,拷贝是指用一份原有的.已经存在的数据创建出一份新的数据,最终的结果是多了一份相同的数据.例如,将 Word 文档拷贝到U盘去复印店打印,将 D 盘的图片拷贝到桌面以方便浏览,将重要的文件上传到百度网盘以防止丢失等,都是「创建一份新数据」的意思. 在

  • C++超详细讲解构造函数

    目录 类的6个默认成员函数 构造函数 特性 编译器生成的默认构造函数 成员变量的命名风格 类的6个默认成员函数 如果我们写了一个类,这个类我们只写了成员变量没有定义成员函数,那么这个类中就没有函数了吗?并不是的,在我们定义类时即使我们没有写任何成员函数,编译器会自动生成下面6个默认成员函数. class S { public: int _a; }; 这里就来详细介绍一下构造函数. 构造函数 使用C语言,我们用结构体创建一个变量时,变量的内容都是随机值,要想要能正确的操作变量中存储的数据,我们还需

  • java反射超详细讲解

    目录 Java反射超详解✌ 1.反射基础 1.1Class类 1.2类加载 2.反射的使用 2.1Class对象的获取 2.2Constructor类及其用法 2.4Method类及其用法 Java反射超详解✌ 1.反射基础 Java反射机制是在程序的运行过程中,对于任何一个类,都能够知道它的所有属性和方法:对于任意一个对象,都能够知道它的任意属性和方法,这种动态获取信息以及动态调用对象方法的功能称为Java语言的反射机制. Java反射机制主要提供以下这几个功能: 在运行时判断任意一个对象所属

  • 超详细讲解Java线程池

    目录 池化技术 池化思想介绍 池化技术的应用 如何设计一个线程池 Java线程池解析 ThreadPoolExecutor使用介绍 内置线程池使用 ThreadPoolExecutor解析 整体设计 线程池生命周期 任务管理解析 woker对象 Java线程池实践建议 不建议使用Exectuors 线程池大小设置 线程池监控 带着问题阅读 1.什么是池化,池化能带来什么好处 2.如何设计一个资源池 3.Java的线程池如何使用,Java提供了哪些内置线程池 4.线程池使用有哪些注意事项 池化技术

  • 超详细讲解Java异常

    目录 一.Java异常架构与异常关键字 Java异常简介 Java异常架构 1.Throwable 2.Error(错误) 3.Exception(异常) 4.受检异常与非受检异常 Java异常关键字 二.Java异常处理 声明异常 抛出异常 捕获异常 如何选择异常类型 常见异常处理方式 1.直接抛出异常 2.封装异常再抛出 3.捕获异常 4.自定义异常 5.try-catch-finally 6.try-with-resource 三.Java异常常见面试题 1.Error 和 Excepti

  • Java 超详细讲解对象的构造及初始化

    目录 如何初始化对象 构造方法 特性 默认初始化 就地初始化 如何初始化对象 我们知道再Java方法内部定义一个局部变量的时候,必须要初始化,否则就会编译失败 要让这串代码通过编译,很简单,只需要在正式使用a之前,给a设置一个初始值就好那么对于创造好的对象来说,我们也要进行相对应的初始化我们先写一个Mydate的类 public class MyDate { public int year; public int month; public int day; /** * 设置日期: */ pub

  • Java超详细讲解类的继承

    目录 写在前面 1.子类的创建 1.1子类的创建方法 1.2调用父类中特定的构造方法 2.在子类中访问父类成员 3.覆盖 3.1覆盖父类中的方法 3.2用父类的对象访问子类的成员 4.不可被继承的成员和最终类 实例java代码 写在前面 类的继承可以在已有类的基础上派生出来新的类,不需要编写重复的代码,提高了代码的复用性,是面向对象程序设计的一个重要的特点,被继承的类叫做父类,由继承产生的新的类叫做子类,一个父类可以通过继承产生多个子类,但是与C++不同的是Java语言不支持多重继承,即不能由多

  • C++超详细讲解内存空间分配与this指针

    目录 成员属性和函数的存储 空对象 成员属性的存储 成员函数的存储 this指针的概念 解决名称冲突 返回对象指针*this 总结 成员属性和函数的存储 在C++中成员变量和成员函数是分开存储的: 空对象 class Person {}; 这里我直接创建一个空的类,并创建一个空的类对象(Person p),利用sizeof关键字输出p所占内存空间,sizeof(p);结果是p=1: 注意:空对象占用内存空间为: 1.C++编译器会给每个空对象分配一个字节空间,是为了区分空对象占内存的位置 2.每

  • C++超详细讲解函数对象

    目录 一.客户需求 二.存在的问题 三.解决方案 四.函数对象 五.小结 一.客户需求 编写一个函数 函数可以获得斐波那契数列每项的值 每调用一次返回一个值 函数可根据需要重复使用 下面来看第一个解决方案: #include <iostream> using namespace std; int fib() { static int a0 = 0; static int a1 = 1; int ret = a1; a1 = a0 + a1; a0 = ret; return ret; } in

随机推荐