[C++ 从入门到精通] 8.构造函数详解、explicit、初始化列表

简介: [C++ 从入门到精通] 8.构造函数详解、explicit、初始化列表

一. 构造函数

在类中,有一种特殊的成员函数——构造函数:它的名字和类名相同,并且在创建类的对象的时候,构造函数函数会被系统自动调用

1、构造函数的目的:初始化类对象的数据成员

下面,我们定义一个public型的构造函数来感受一下构造如何初始化数据成员:

//.h
class Time
{
public:
  int Hour;
  int Minute;
  int Second;
public:
    //定义构造函数
    Time(int tmphour, int tmpmin, int tmpsec);
};
//.cpp 构造函数的实现
Time::Time(int tmphour, int tmpmin, int tmpsec)
{
    Hour = tmphour;
    Minute = tmpmin;
    Second = tmpsec;
}

2、构造函数的特点:

  • 构造函数没有返回值,这也是构造函数的特殊之处。
  • 不可以手动调用构造函数,否则编译会出错。
  • 正常情况下,构造函数应该被手动声明为public:因为我们创建一个类对象时,系统要替我们调用构造函数,这说明构造函数需要是一个public函数,又因为类缺省的成员是private类型的,所以我们需要在构造函数前声明为public类型的才能被系统自动调用。
  • 如果构造函数中有多个参数,则我们创建类对象的时候也要加上这些参数类型。

注:这种创建类对象调用构造函数初始化类成员的方式和上一篇我们说的对象拷贝其实差不多,但是对象拷贝调用的不是本文介绍的传统的构造函数,而是调用的拷贝构造函数(后文讲解)。


二. 构造函数多参数情况

一个类中可以有多个构造函数,多个构造函数意味着可以为类对象的创建提供了多种初始化方式(这多个构造函数之间的区别视自己的需求而定,比如构造函数参数的数量类别不同)。

继续使用上面的类Time举例:

public:
    //定义构造函数1
    Time(int tmphour, int tmpmin, int tmpsec);
    //定义构造函数2,
    Time();  //无参
//.cpp 
Time::Time(int tmphour, int tmpmin, int tmpsec)  //构造函数1的实现
{
    Hour = tmphour;
    Minute = tmpmin;
    Second = tmpsec;
}
Time::Time()  //构造函数2的实现
{
    Hour = 12;
    Minute = 27;
    Second = 35;
}
int main()
{
    Time myTime1(12,30,50);         //调用构造函数1(方式1)
    Time myTime2{ 12,30,50 };       //调用构造函数1(方式2)
    Time myTime3 = Time(12,30,50);  //调用构造函数1(方式3)
    Time myTime4 = Time{12,30,50};  //调用构造函数1(方式4)
    Time myTime5 = { 12,30,50 };    //调用构造函数1(方式5)
    Time myTime1;             //调用构造函数2。注意无参构造函数不用Time myTime1()创建类对象
    Time myTime2 = Time();    //调用构造函数2
    Time myTime3{};           //调用构造函数2
    Time myTime4 = {};        //调用构造函数2
    Time myTime5 = Time{};    //调用构造函数2
}

三. 函数默认参数

1、定义: 函数默认参数,即为函数的参数设定一个默认值,我们称这个参数为函数的默认参数(默认有值的参数)。

2、规定:

  • 参数的默认值只能放在.h函数声明中。除非该函数没有声明,只有定义。
  • 具有多个参数的函数中指定默认值时默认参数必须出现在非默认参数的右边,一旦某个参数开始指定默认值,它右边所有参数必须指定默认值。
  • 声明构造函数时有m个参数,n个参数有默认值(0,nullptr或其他值),这意味着定义构造函数时,默认参数可以不必须给初始值,但是非默认参数必须给初始值。

示例:

public:
    //多个参数的构造函数
    Time(int tmphour, int tmpmin = 10, int tmpsec);   //错误
    Time(int tmphour, int tmpmin, int tmpsec = 10);   //正确
Time::Time(int tmphour, int tmpmin, int tmpsec)
{
  Hour = tmphour;
  Minute = tmpmin;
  Second = tmpsec;
  cout << "Time的构造函数被调用" << endl;
  cout << "Time成员变量Hour的值" << Hour << endl;
  cout << "Time成员变量Minute的值" << Minute << endl;
  cout << "Time成员变量Second的值" << Second << endl;
}
int main()
{
    Time myTime(12,30);    //调用构造函数
}

可以看出,这里由于构造函数Time(int tmphour, int tmpmin, int tmpsec = 10)的3个参数中有一个默认参数int tmpsec = 10,所以我们初始化Time myTime(12,30)时,默认参数没有给值,只给出了两个非默认参数的初始值,这种情况是被允许的。


四. 隐式转换和explicit

1、隐式转换的出现

前面我们已经了解过数据转换之间的隐式转换了。下面,我们看一下构造函数中的隐式转换情况,假设构造函数只有一个参数:

public:
    //定义构造函数
    Time(int tmphour);
Time::Time(int tmphour)
{
  Hour = tmphour;
  cout << "Time的构造函数被调用" << endl;
}
void fun(Time tmp)
{
    return;
}
int main()
{
    Time myTime =  Time{16};  //正常初始化,调用了单参数的构造函数
    Time myTime1 = 16;        //这种含糊不清的写法(将int类型Time类型是不被允许的),但是编译器对其进行了隐式转换,使其可以正常调用构造函数
    fun(16);                  //同上,也是含糊不清的写法,存在临时对象或隐式转换的问题,编译器将实参16赋值给了形参Time tmp时,也默认做了隐式转换
}

我们可以看到,后面两种含糊不清的写法,其语法也没问题并且成功调用了构造函数,这是因为编译器自动对其做了隐式转换。这两种写法虽然语法没什么问题,但是总归看了会让人有一些不舒服的,那么能否要求构造函数不能做隐式转换呢?

2、禁止构造函数做隐式转换——explicit

如果构造函数声明中带有关键字explicit,则这个构造函数只能用于初始化和显式类型转换。

以带3参数构造函数为例

public:
    //3个参数的构造函数定义
    explicit Time(int tmphour, int tmpmin, int tmpsec);   //加入关键字explicit,禁止隐式转换
Time::Time(int tmphour, int tmpmin, int tmpsec)
{
  Hour = tmphour;
  Minute = tmpmin;
  Second = tmpsec;
}
int main()
{
    Time myTime1(12,30,50);         //调用构造函数1(方式1)
    Time myTime2{ 12,30,50 };       //调用构造函数1(方式2)
    Time myTime3 = Time(12,30,50);  //调用构造函数1(方式3)
    Time myTime4 = Time{12,30,50};  //调用构造函数1(方式4)
    Time myTime5 = { 12,30,50 };    //调用构造函数1(方式5)
}

我们看到,给构造函数加入限定词explicit后,调用方式5(复制列表初始化)——Time myTime5 = { 12,30,50 }报错,说明= {}方式是隐式类型转换。

以带1参数构造函数为例

public:
    //1个参数的构造函数定义
    explicit Time(int tmphour);   //加入关键字explicit,禁止隐式转换
Time::Time(int tmphour)
{
  Hour = tmphour;
}
void fun(Time tmp)
{
  return;
}
int main()
{
  Time myTime1(16);           //调用构造函数
  Time myTime2 = Time(16);    //调用构造函数
  Time myTime3{16};           //调用构造函数
  Time myTime4 = {16};        //调用构造函数,隐式类型转换
  Time myTime5 = Time{16};    //调用构造函数
  Time myTime6 = 16;          //调用构造函数(含糊不清),隐式类型转换
  fun(16);                    //调用构造函数(含糊不清),隐式类型转换
}

可以看到,隐式类型转换三个方式原形毕露了,可以通过上面另外四种显示类型转换的方式对他们三个进行修改:

Time myTime4 = Time{16};      
Time myTime6 = Time(16);       
fun(Time(16));

对于单参数构造函数,如果没有特殊理由,一般都声明为explicit,防止系统将一个数字默认隐式转换成对象(int/double)。也包括无参数和多参数的构造函数。


五. 构造函数初始化列表

上面我们通过构造函数对类成员变量初始化赋值时,是通过:

//.h
class Time
{
public:
  int Hour;
  int Minute;
  int Second;
public:
    //定义构造函数
    Time(int tmphour, int tmpmin, int tmpsec);
};
//.cpp
Time::Time(int tmphour, int tmpmin, int tmpsec)  
{
  Hour = tmphour;                 //初始化成员函数(赋值)
  Minute = tmpmin; 
  Second = tmpsec;
}

其实还有更推荐的初始化成员变量写法,就是构造函数初始化列表,推荐原因有两点:写法更专业,效率更高(上面构造函数多做了一个赋值的步骤)。

构造函数初始化列表写法如下:

Time::Time(int tmphour, int tmpmin, int tmpsec) : 
    Hour(tmphour),
    Minute(tmpmin),
    Second(tmpsec)                   //初始化成员函数(直接初始化,上面是省略初始化步骤,加了一个赋值步骤,虽然都是初始化成员变量,但这种效率会高一点点)
{
  //Hour = tmphour;
  //Minute = tmpmin;
  //Second = tmpsec;
}

有一点需要注意,有人有时候可能会用一个成员变量去初始化赋值另一个成员变量,如用成员变量Hour初始化Minute的值:

Time::Time(int tmphour, int tmpmin, int tmpsec) : 
    Hour(tmphour),
    Minute(Hour),
    Second(tmpsec)                   
{
}

上面有人这么写的目的是认为构造函数初始化列表先初始化的成员变量是HourHour有值了然后再将Hour的值初始化Minute,这个逻辑其实没问题。

但是要注意构造函数初始化列表先初始化谁要取决于头文件.h先定义哪个成员变量,本文先定义的int Hour,所以这么写没问题,但是如果先定义int Minute,那么构造函数初始化列表就会先初始化Minute,而这时Hour是没有值的,所以会出问题,大家注意不要用这种方式。


六. 总结

本文主要讲解的知识点如下:

  1. 构造函数的目的就是初始化类对象的数据成员。
  2. 创建类对象的参数类型要和构造函数中的参数类型相匹配(顺序和个数)。
  3. 定义构造函数的默认参数(0、nullptr或其它默认值)要放到头文件中,且统一在非默认参数的右边。
  4. 其他创建类对象初始化构造函数时,头文件声明的默认参数可以不必须给初始值,但是非默认参数必须给初始值。
  5. 禁止构造函数做隐式转换(= {}初始化方式)——explicit
  6. 学会使用构造函数初始化列表的方式来初始化类成员变量。

下雨天,最惬意的事莫过于躺在床上静静听雨,雨中入眠,连梦里也长出青苔。


目录
相关文章
|
21天前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
54 5
|
27天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
56 4
|
2月前
|
编译器 C++
C++入门12——详解多态1
C++入门12——详解多态1
42 2
C++入门12——详解多态1
|
2月前
|
C++
C++入门13——详解多态2
C++入门13——详解多态2
84 1
|
2月前
|
存储 安全 编译器
【C++打怪之路Lv1】-- 入门二级
【C++打怪之路Lv1】-- 入门二级
24 0
|
2月前
|
自然语言处理 编译器 C语言
【C++打怪之路Lv1】-- C++开篇(入门)
【C++打怪之路Lv1】-- C++开篇(入门)
34 0
|
2月前
|
分布式计算 Java 编译器
【C++入门(下)】—— 我与C++的不解之缘(二)
【C++入门(下)】—— 我与C++的不解之缘(二)
|
2月前
|
编译器 Linux C语言
【C++入门(上)】—— 我与C++的不解之缘(一)
【C++入门(上)】—— 我与C++的不解之缘(一)
|
15天前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
25 2
|
28天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
66 4