C++:类中构造函数的由来

231 阅读8分钟

构造函数


类的6个默认成员函数

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

在这里插入图片描述

构造函数

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;
    d1.SetDate(2018,5,1);
    d1.Display();
    
    Date d2;
    d2.SetDate(2018,7,1);
    d2.Display();
    return 0;
}

对于Date类,可以通过SetDate公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?

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

构造函数特性

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

其特征如下:

  1. 函数名与类名相同。
  2. 无返回值。
  3. 对象实例化时编译器自动调用对应的构造函数。(并且在对象的生命周期内只调用一次)
  4. 构造函数可以重载。
class Date {
public :
    // 1.无参构造函数
    Date () {}
    // 2.带参构造函数
    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 (2015, 1, 1); // 调用带参的构造函数

    // 注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明 
    // 以下代码的函数:声明了d3函数,该函数无参,返回一个日期类型的对象
    Date d3();
    
    return 0;
}
  1. 如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date
{
public:
    /*
    // 如果用户显式定义了构造函数,编译器将不再生成 
    Date (int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
    }
*/
private:
    int _year;
    int _month;
    int _day; 
};

int main() {
    // 没有定义构造函数,对象也可以创建成功,因此此处调用的是编译器生成的默认构造函数
    Date d; 

	return 0;
}
  1. 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、编译器默认生成的构造函数(没有写),都可以认为是默认构造函数。
// 默认构造函数 
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;
    }
private :
    int _year ;
    int _month ;
    int _day ; 
};

// 以下测试函数能通过编译吗? 
void Test()
{
    Date d1; //不可以(类Date中包含多个默认构造函数)
}
  1. 关于编译器生成的默认成员函数,我们会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d对象调用了编译器生成的默认构造函数,但是d对象year/month/_day,依旧是随机值。也就说在这里 编译器生成的默认构造函数并没有什么卵用??

解答:

C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语法已经定义好的类型:如 int/char…,自定义类型就是我们使用class/struct/union自己定义的类型。通过下面的程序,可以发现 编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。

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

class Date
{
private:
    // 基本类型(内置类型) 
    int _year;
    int _month;
    int _day;
    // 自定义类型
    Time _t; 
};

int main() {
    Date d;	// 输出:Time()
    return 0;
}
  1. 成员变量的命名风格
// 我们看看这个函数,是不是很僵硬? 
class Date
{
public:
    Date(int year)
    {
    // 这里的year到底是成员变量,还是函数形参?
    year = year; 
    }
private:
    int year;
};

//建议这样(全局变量g_; 局部静态变量s_; 成员变量m_)
class Date
{
public:
    Date(int year)
    {
        m_year = year;
    }
private:
    int m_year;
};

要点总结:

  1. 它是一个特殊的成员函数,它不存在返回值,名字和类名相同,在 实例化对象的时候自动调用
  2. 系统会自动提供一个默认的构造函数,如果自己实现了构造函数,则系统不再提供默认的构造函数。
  3. 构造函数可以存在参数,他与其他的构造函数是以函数重载的方式共同存在的。

构造函数体赋值

在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。

 class Date
{
public:
    Date(int year, int month, int day)
    {
        _year = year;
        _month = month;
        _day = day;
}
private:
    int _year;
	int _month;
	int _day; 
};

虽然上述构造函数调用之后,对象中已经有了一个初始值,但是不能将其称作为类对象成员的初始化,构造函数体中的语句只能将其称作为赋初值,而不能称作初始化。因为初始化只能初始化一次,而构造函数体内可以多次赋值

初始化列表

初始化列表:以一个 冒号开始,接着是一个以 逗号分隔的数据成员列表,每个"成员变量"后面跟一个 放在括号中的初始值或表达式

class Date
{ 
public:
    Date(int year, int month, int day)
        : _year(year)
        , _month(month)
        , _day(day)
    {}
private:
    int _year;
	int _month;
	int _day; 
};

注意:

  1. 每个成员变量在初始化列表中 只能出现一次(初始化只能初始化一次)

  2. 类中包含以下成员,必须放在初始化列表位置进行初始化:

    • 引用成员变量
    • const成员变量 (C++11:则直接可以对const可以直接初始化)
    • 类类型成员(该类没有默认构造函数)
    • 如果类存在继承关系,派生类必须在其初始化列表中调用基类的构造函数。即子类初始化父类的私有成员。
class A
{
public:
    A(int a)
        :_a(a)
	{} 
private:
	int _a; 
};

class B
{
public:
    B(int a, int ref)
        :_aobj(a)
		,_ref(ref)
		,_n(10) 
	{}
private:
	A _aobj; // 没有默认构造函数 
	int& _ref; // 引用
	const int _n; // const
};
#include<iostream>
using namespace std;

//基类People
class People {
protected:
    const char* m_name;
    int m_age;
public:
    People(const char*, int);
};
People::People(const char* name, int age) : m_name(name), m_age(age) {}

//派生类Student
class Student : public People {
private:
    float m_score;
public:
    Student(const char* name, int age, float score);
    void display();
};
//People(name, age)就是调用基类的构造函数
Student::Student(const char* name, int age, float score) : People(name, age), m_score(score) { }
void Student::display() {
    cout << m_name << "的年龄是" << m_age << ",成绩是" << m_score << "。" << endl;
}

int main() {
    Student stu("小明", 16, 90.5);
    stu.display();

    return 0;
}
  1. 类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。
    对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别;
    非内部数据类型的成员对象应当采初始化列表,以获取更高的效率。 例如:
class A
{…
	A(void);				// 无参数构造函数
	A(const A &other);		// 拷贝构造函数
	A & operate =( const A &other);	// 赋值函数
};

class B
{
public:
	B(const A &a);	// B的构造函数
private:	
	A  m_a;			// 成员对象
};
  • 示例1 中,类B的构造函数在其初始化表里调用了类A的拷贝构造函数,从而将成员对象m_a初始化。
  • 示例2 中,类B的构造函数在函数体内用赋值的方式将成员对象m_a初始化。我们看到的只是一条赋值语句,但实际上B的构造函数干了两件事:先暗地里创建m_a对象(调用了A的无参数构造函数),再调用类A的赋值函数,将参数a赋给m_a。
// 示例 1
B::B(const A &a)
 : m_a(a)			
{ 
   … 
}

// 示例 2
B::B(const A &a)
{
	m_a = a;
	…
}
  1. 成员变量 在类中 声明次序 就是其在初始化列表中的 初始化顺序,与其在初始化列表中的先后次序无关!!!
class Array
{
public:
    Array(int size)
        :_size(size)
        , _array((int*)malloc(sizeof(int)*_size))
    {}
private:
    int* _array;
	int _size; 
};

提问知识点:

  1. 构造函数能不能用const来修饰?
  • const修饰函数表示该函数的返回值是const类型的,该返回值只能赋值给同类型的const变量。
  • const也可以修饰类的成员函数,但是该函数不能修改数据成员。构造函数也属于类的成员函数,但是 构造函数是要修改类的成员变量,所以类的构造函数不能申明成const类型的。
  1. static能修饰构造函数吗?

构造函数不能够使用static修饰

  • 因为被static修饰的成员,是随着类的加载而存在,无须创建对象而可以直接被调用,而构造函数是给对象初始化的,它的存在必须依赖于对象而存在。
  • 构造函数主要是针对非静态成员变量进行初始化,而静态成员函数一般只能直接操作静态成员变量
  1. 构造函数能不能是虚函数? <------ 请点击本文:不能

狭义 & 广义初始化

  • 狭义初始化:指的是 在定义变量的时候直接进行初始化 的这种行为叫做狭义初始化
int a = 3;
  • 广义初始化:第一次给变量赋值 就叫做初始化的情况叫做广义初始化
int a;
…//跟 a 无关的代码 
a = 3

初始化列表相当于狭义初始化,而构造函数内部相当于广义初始化

习题
  1. 下面说法正确的是()

A. 一个空类默认一定生成构造函数,拷贝构造函数,赋值操作符,引用操作符,析构函数
B. 可以有多个析构函数
C. 析构函数可以为virtual,可以被重载
D. 类的构造函数如果都不是public访问属性,则类的实例无法创建

正确答案:

A

答案解析:

A. 空类中有六个默认的成员函数: 1:构造函数 2:拷贝构造函数 3:析构函数 4:=运算符重载函数 5: &取址运算符重载 6:const修饰的取址运算符重载(并返回const指针)
B:一个类只会有一个析构函数
C:析构函数没有参数列表,无法重载,但是可以重写
D:单例模式(意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。)

class  CSingleton
{
private:
	CSingleton()   //构造函数是私有的   
	{
	}
	static  CSingleton* m_pInstance;
public:
	static  CSingleton* GetInstance()
	{
		if (m_pInstance == NULL)   //判断是否第一次调用   
			m_pInstance = new  CSingleton();
		return m_pInstance;
	}
};

用户访问唯一实例的方法只有GetInstance()成员函数。如果不通过这个函数,任何创建实例的尝试都将失败,因为类的构造函数是私有的。GetInstance()使用懒惰初始化,也就是说它的返回值是当这个函数首次被访问时被创建的。这是一种防弹设计——所有GetInstance()之后的调用都返回相同实例的指针

ClassA *pclassa=new ClassA[5]; 
delete pclassa;

c++语言中,类ClassA的构造函数和析构函数的执行次数分别为()

A 5,1
B 1,1
C 5,5
D 1,5

正确答案:

A

答案解析:

Class A *pclassa=newClassA[5];
new了五个对象,所以构造5次,
然后Pclass指向这五个对象deletepclassa; 析构一次,
delete[]pclassa 这样就析构5次

  1. 有一个类B继承自类A,他们数据成员如下:
class A {
	...
privateint& a;
};
class B : public A {
	...
private:
	int a;
public:
	const int b;
	A &c;
	static const char* d;
	A* e;
};

则构造函数中,成员变量一定要通过初始化列表来初始化的是__

A. a b c
B. b c e
C. b c d e
D. c e
E. b d
F. b c

正确答案

F

答案解析

构造函数初始化时必须采用初始化列表一共有三种情况,

  • 需要初始化的数据成员是对象(继承时调用基类构造函数)
  • 需要初始化const修饰的类成员
  • 需要初始化引用成员数据

static 修饰的变量在类外初始化(全局区)

指针和引用的区别。我们在定义一个引用的时候必须要初始化的,而且不能更改。指针就不一样了,不仅不必初始化,而且可以更改指向。 (所以e可以不是通过初始化列表来初始化)
普通变量可以在初始化列表也可以在构造函数里面赋值

  1. 假定CSomething是一个类,执行下面这些语句之后,内存里创建了____个CSomething对象。
CSomething a();
CSomething b(2);
CSomething c[3];
CSomething &ra = b;
CSomething d=b;
CSomething *pA = c;
CSomething *p = new CSomething(4);

A. 10
B. 9
C. 8
D. 7
E. 6
F. 5

正确答案

E

答案解析

CSomething a();		// 没有创建对象,这里不是使用默认构造函数,而是定义了一个函数
CSomething b(2);	// 使用一个参数的构造函数,创建了一个对象。
CSomething c[3];	// 使用无参构造函数,创建了3个对象。
CSomething &ra=b;	// ra引用b,没有创建新对象。
CSomething d=b;		// 使用拷贝构造函数,创建了一个新的对象d。
CSomething *pA = c;	// 只是给指针赋值
CSomething *p = new CSomething(4);	// 新建一个对象,构造并给指针赋值
  1. 若PAT是一个类,则程序运行时,语句“PAT(*ad)[3];”调用PAT的构造函数的次数是( )。

A. 2
B. 3
C. 0
D. 1

正确答案

C

答案解析

语句“PAT(*ad) [3]”定义了含有3个元素的指向类PAT类型的指针数组ad。

  1. 以下代码有什么问题?
struct Test
 {
     Test( int ) {}
     Test() {}
     void fun() {}
 };
 void main( void )
 {
     Test a(1);
     a.fun();
     Test b();
     b.fun();
 }

A. b.fun()会出错
B. Test结构的定义中应该加上public修饰符,这样才能main函数中调用改类的方法
C. Test(int){} 应该改成Test(int a){}
D. 以上说法都不正确

正确答案

A

答案解析

其实Test b();并不会出错,只不过是声明了一个函数b返回类型为Test

默认构造函数直接写 Test b;就可以

  1. 定义一个空的类型,里面没有任何成员变量和成员函数。对该类型求 sizeof ,得到的结果是多少?

答案是 1

为什么不是0?

空类型的实例不包含任何信息,本来求 sizeof 应该是0, 但是当我们声明该类型的实例的时候,他必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。在 Visual Studio 中,每个空类型的实例占用1字节的空间。

在该类型加上构造函数和析构函数呢?

结果还是1。调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关,编译器也不会因为指两个函数在实例内添加任何额外的信息。


如果有不同意见,欢迎留言讨论呀!