构造函数
类的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公有的方法给对象设置内容,但是如果每次创建对象都调用该方法设置信息,未免有点麻烦,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,保证每个数据成员都有一个合适的初始值,并且 在对象的生命周期内只调用一次。
构造函数特性
构造函数是特殊的成员函数,需要注意的是,构造函数的虽然名称叫构造,但是需要注意的是构造函数的主要任务 并不是开空间创建对象,而是初始化对象。
其特征如下:
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。(并且在对象的生命周期内只调用一次)
- 构造函数可以重载。
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;
}
- 如果类中没有显式定义构造函数,则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;
}
- 无参的构造函数和全缺省的构造函数都称为默认构造函数,并且默认构造函数只能有一个。 注意:无参构造函数、全缺省构造函数、编译器默认生成的构造函数(没有写),都可以认为是默认构造函数。
// 默认构造函数
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中包含多个默认构造函数)
}
- 关于编译器生成的默认成员函数,我们会有疑惑:在我们不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?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;
}
- 成员变量的命名风格
// 我们看看这个函数,是不是很僵硬?
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;
};
要点总结:
- 它是一个特殊的成员函数,它不存在返回值,名字和类名相同,在 实例化对象的时候自动调用。
- 系统会自动提供一个默认的构造函数,如果自己实现了构造函数,则系统不再提供默认的构造函数。
- 构造函数可以存在参数,他与其他的构造函数是以函数重载的方式共同存在的。
构造函数体赋值
在创建对象时,编译器通过调用构造函数,给对象中各个成员变量一个合适的初始值。
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;
};
注意:
-
每个成员变量在初始化列表中 只能出现一次(初始化只能初始化一次)
-
类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- 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;
}
- 类的数据成员的初始化可以采用初始化表或函数体内赋值两种方式,这两种方式的效率不完全相同。
对于内部数据类型的数据成员而言,两种初始化方式的效率几乎没有区别;
非内部数据类型的成员对象应当采初始化列表,以获取更高的效率。 例如:
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;
…
}
- 成员变量 在类中 声明次序 就是其在初始化列表中的 初始化顺序,与其在初始化列表中的先后次序无关!!!
class Array
{
public:
Array(int size)
:_size(size)
, _array((int*)malloc(sizeof(int)*_size))
{}
private:
int* _array;
int _size;
};
提问知识点:
- 构造函数能不能用const来修饰?
- const修饰函数表示该函数的返回值是const类型的,该返回值只能赋值给同类型的const变量。
- const也可以修饰类的成员函数,但是该函数不能修改数据成员。构造函数也属于类的成员函数,但是 构造函数是要修改类的成员变量,所以类的构造函数不能申明成const类型的。
- static能修饰构造函数吗?
构造函数不能够使用static修饰
- 因为被static修饰的成员,是随着类的加载而存在,无须创建对象而可以直接被调用,而构造函数是给对象初始化的,它的存在必须依赖于对象而存在。
- 构造函数主要是针对非静态成员变量进行初始化,而静态成员函数一般只能直接操作静态成员变量
狭义 & 广义初始化
- 狭义初始化:指的是 在定义变量的时候直接进行初始化 的这种行为叫做狭义初始化
int a = 3;
- 广义初始化:第一次给变量赋值 就叫做初始化的情况叫做广义初始化
int a;
…//跟 a 无关的代码
a = 3;
初始化列表相当于狭义初始化,而构造函数内部相当于广义初始化
习题
- 下面说法正确的是()
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次
- 有一个类B继承自类A,他们数据成员如下:
class A {
...
private:
int& 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可以不是通过初始化列表来初始化)
普通变量可以在初始化列表也可以在构造函数里面赋值
- 假定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); // 新建一个对象,构造并给指针赋值
- 若PAT是一个类,则程序运行时,语句“PAT(*ad)[3];”调用PAT的构造函数的次数是( )。
A. 2
B. 3
C. 0
D. 1
正确答案
C
答案解析
语句“PAT(*ad) [3]”定义了含有3个元素的指向类PAT类型的指针数组ad。
- 以下代码有什么问题?
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;就可以
- 定义一个空的类型,里面没有任何成员变量和成员函数。对该类型求 sizeof ,得到的结果是多少?
答案是 1
为什么不是0?
空类型的实例不包含任何信息,本来求 sizeof 应该是0, 但是当我们声明该类型的实例的时候,他必须在内存中占有一定的空间,否则无法使用这些实例。至于占用多少内存,由编译器决定。在 Visual Studio 中,每个空类型的实例占用1字节的空间。
在该类型加上构造函数和析构函数呢?
结果还是1。调用构造函数和析构函数只需要知道函数的地址即可,而这些函数的地址只与类型相关,而与类型的实例无关,编译器也不会因为指两个函数在实例内添加任何额外的信息。
如果有不同意见,欢迎留言讨论呀!