C++中 类的构造函数理解(一)

写在前面

这段时间完成三个方面的事情:
1、继续巩固基础知识(主要是C++ 方面的知识)
2、尝试实现一个iOS的app,通过完成app,学习iOS开发中要用到的知识
3、完善实验室的研究项目,为毕业设计做准备
有了这三个安排之后,就可以把一天的时间大致分为三份了。对于C++ 知识点的学习这部分,主要是看《C++ Primer》以及本科使用的英文教材《C++:How to program》来进行,今天主要探索一下C++ 中类的构造函数。

类简介

什么是类
类(Class)是C++ 提供的一种抽象数据类型,用户可以定义自己的“类”来自定义数据类型。类可以说是C++ 中最重要的特征之一。为什么会出现类呢?我的理解是,C++ 中内置的数据类型(int、double、char等)不能满足用户处理复杂事务时的需求,因此C++ 中提供了一种抽象机制,允许用户DIY自己的数据类型,在DIY自己的数据类型也就是类的时候,需要告诉编译器,你的类名字是什么?它在哪里定义?它有哪些属性(数据成员)以及它可以有哪些操作(成员函数),把这套规则都定义好之后,用户就能在程序中操作自己的类以完成某些任务了。
类的数据成员
在上述简介中,提到了类的数据成员,那么类的数据成员到底是什么呢?“数据成员”这个说法是我学习《C++: How to program》时接触到的,很多人也将“数据成员”称为“成员变量”。简单的说,数据成员就是类的某些“属性”,比如学生类,对应的属性可能就有学号、姓名、专业、年级、课程等,这些属性可以用C++ 内置的数据类型来表示,也可以用一些结构体来表示,甚至也可以用其他类类型来表示。换句话说,类的数据成员是可以被操作的,类中的变量。
类的成员函数
成员函数和普通的函数功能是差不多的,它主要是用来操作数据成员的,或者说用来完成类的某些功能的。
更多类的知识本文就不再扩展了,本文主要探索类的构造函数的一些特性。

构造函数

构造函数简介
创建完类之后,我们在使用类之前,需要对类进行初始化。定义如何对类进行初始化的成员函数就称为构造函数。构造函数在创建类类型的对象时被执行。它的工作是保证每个对象的数据成员具有合适的初始值。构造函数的名字必须与类名字相同,它可以接受参数(当然也可以没有参数),同时也可以定义多个构造函数(也就是说构造函数允许重载)。但有一点要注意的是构造函数不能有返回值,就算是void也不行。另外,由于构造函数设计的初衷是为对象初始化,即初始化对象的数据成员,因此构造函数不能声明为const,否则程序报错。另外,一般情况下构造函数都是公有的,因为构造函数声明为私有的话,其他类将不能生成该类的对象。只有在单例模式下,才将构造函数声明为私有的,以防止类有多个实例出现。
默认构造函数
参数列表为空的构造函数称为默认构造函数。(这个定义不全面,下文中有提到,所有参数都指定了默认值的带参数的构造函数也是默认构造函数)

  • 编译器合成的默认构造函数

我们已经知道构造函数的作用是定义类的初始化,那么如果我们不显示的定义构造函数,编译器能为我们创建一个类的实例(对象)吗?(这里啰嗦一句,类的对象也称为类的实例,二者是等价的概念,本文两种说法都有使用)。答案是肯定的,这是因为,当用户没有显示定义类的构造函数时,编译器会为该类生成一个默认的构造函数,也称为合成的构造函数。默认构造函数不会初始化对象的数据成员,但如果当前对象的数据成员中包含其他类类型,那么默认构造函数会调用其他类的默认构造函数。其形式如下:

  1. class MyClass
  2. {
  3. public:
  4. MyClass(){} // default constructor
  5. } ;

编译器不合成默认构造函数的情况
(1)一旦用户定了构造函数之后(只要有一个,无论是公有还是私有,无论哪种形式,哪怕用户定义的形式与编译器合成的形式相同),编译器就不再为用户合成默认的构造函数。
(2)如果类包含内置或复合类型的成员,则不能依赖编译器来合成默认构造函数,因为编译器无法初始化复合类型的成员

  • 用户自定义的默认构造函数

用户也可以显示的为自己的类定义默认构造函数,其形式如下:

  1. class MyClass
  2. {
  3. public:
  4. MyClass()
  5. {
  6. cout<<"Default constructor called."<<endl;
  7. m1_int = 7;
  8. m2_double = 0.8;
  9. } // default constructor
  10. private:
  11. int m1_int;
  12. double m2_double;
  13. } ;

定义了上述形式的默认构造函数之后,如果声明一个MyClass的myclass对象,则默认构造函数就会被调用,myclass的两个数据成员就能被初始化。

  • 类没有默认构造函数时会出现的情况

假定有一个类NoDefault,它没有默认构造函数,但有一个接受一个string实参的构造函数,因此编译器不会为NoDefault类合成默认的构造函数,这意味着:
(1)具有NoDefault成员的每个类的每个构造函数,必须通过传递一个初始的string值给NoDefault构造函数来显示地初始化NoDefault成员。
(2)编译器不会为具有NoDefault成员的类合成默认构造函数。如果这样的类希望提供默认构造函数,必须显示地定义,并且默认构造函数必须显示地初始化其NoDefault成员。
(3)NoDefault类型不能用作动态分配数组的元素类型。
(4)NoDefault类型的静态分配数组必须为每个元素提供一个显示的初始化式。
(5)如果有一个保存NoDefault对象的容器,例如vector,就不能使用接受容器大小而没有同时提供一个元素初始化式的构造函数。

构造函数初始化列表
构造函数化初始化列表形式如下:

  1. class MyClass
  2. {
  3. public:
  4. //第一种形式
  5. MyClass() : m1_int(7), m2_double(0.8)
  6. {
  7. cout<<"Constructor 1 is called"<<endl;
  8. }
  9. //第二种形式
  10. MyClass(int m1, double m2) : m1_int(m1), m2_double(m2)
  11. {
  12. cout<<"Constructor 2 is called"<<endl;
  13. }
  14. private:
  15. int m1_int;
  16. double m2_double;
  17. } ;

构造函数初始化列表在构造函数名后添加一个冒号,冒号后是以逗号分隔的数据成员列表,每个数据成员后跟一个放在圆括号中的初始化形式。圆括号中的初始化形式可以是任意复杂的表达式。
这里提一下,类的数据成员被初始化的顺序是其定义的次序,而与构造函数初始化列表中的顺序无关。
在main函数中生成两个对象,观察构造函数被调用情况


  1. int main()
  2. {
  3. cout<<"Test in main"<<endl;
  4. MyClass myclass1;//调用第一种形式的构造函数
  5. MyClass myclass(2,0.3);//调用第二种形式的构造函数
  6. //或者用下面的方式创建对象
  7. MyClass myclass1 = MyClass();//调用第一种形式的构造函数
  8. MyClass myclass2 = MyClass(2,0.3);//调用第二种形式的构造函数
  9. system("pause");
  10. return 0;
  11. }

输出结果如下:

值得一提的是,下面这句话的声明是正确的,可以通过编译

  1. MyClass myclass1();

但这个时候我们并不是声明了一个对象,而是声明了类的一个函数。
默认实参的构造函数
默认实参的构造函数确保,在调用构造函数的时候,就算没有提供参数值,构造函数仍然可以正确的初始化类的对象。如果构造函数的所有参数都指定了默认值,这时候的构造函数也属于默认构造函数。同一个类只能有一个默认构造函数,例如下面的类Time

  1. class Time
  2. {
  3. public:
  4. Time( int = 0, int = 0, int = 0);
  5. Time(){}
  6. private:
  7. int hour;
  8. int minute;
  9. int second;
  10. } ;

在Time中,提供了一个所有参数都指定了默认值的带默认参数的构造函数,然后又提供了一个默认构造函数,当不创建任何对象的时候,是可以通过编译的,一旦创建对象,将无法通过编译,因为编译器不知道应该使用哪个默认构造函数来初始化对象,因此报错:

接下来继续看下默认参数的构造函数的列子

  1. class Time
  2. {
  3. public:
  4. Time( int h = 0, int m = 0, int s = 0)
  5. {
  6. setTime(h,m,s);
  7. }
  8. void setTime(int h, int m, int s)
  9. {
  10. setHour( h );
  11. setMinute( m );
  12. setSecond( s );
  13. }
  14. void setHour(int h)
  15. {
  16. hour = h;
  17. }
  18. int getHour()
  19. {
  20. return hour;
  21. }
  22. /////////////////////
  23. void setMinute( int m)
  24. {
  25. minute = m;
  26. }
  27. int getMinute()
  28. {
  29. return minute;
  30. }
  31. /////////////////////
  32. void setSecond( int s )
  33. {
  34. second = s;
  35. }
  36. int getSecond()
  37. {
  38. return second;
  39. }
  40. private:
  41. int hour;
  42. int minute;
  43. int second;
  44. } ;

测试代码

  1. int main()
  2. {
  3. cout<<"Test in main"<<endl;
  4. Time t1;//所有参数都使用默认值
  5. Time t2( 2 );//时指定,分和秒为默认值
  6. Time t3( 21, 24 );//时和分指定,秒为默认值
  7. Time t4( 12, 25, 34 );//所有参数都指定
  8. cout<<"Time of t1:"<<t1.getHour()<<" : "<<t1.getMinute()<<" : "<<t1.getSecond()<<endl;
  9. cout<<"Time of t2:"<<t2.getHour()<<" : "<<t2.getMinute()<<" : "<<t2.getSecond()<<endl;
  10. cout<<"Time of t3:"<<t3.getHour()<<" : "<<t3.getMinute()<<" : "<<t3.getSecond()<<endl;
  11. cout<<"Time of t4:"<<t4.getHour()<<" : "<<t4.getMinute()<<" : "<<t4.getSecond()<<endl;
  12. system("pause");
  13. return 0;
  14. }

输出如下:

可以看到,调用带默认参数的构造函数时,对象的数据成员的缺省顺序与构造函数中的顺序相反。

总结

本文对构造函数的探索暂时就到这里了,通过今天的探索,发现构造函数这一个知识点中蕴藏着相当多的内容,本文只是做了一个简单的概括。关于构造函数,还有很多都没有探究,比如构造函数之间的调用、拷贝构造函数、有多种类型的数据成员时(如全局动态、全局静态等)各成员被创建的时机、有类的继承发生时,构造函数的处理情况等等,这些内容就留待下篇文章去探索了。

C++中 类的构造函数理解(一)的更多相关文章

  1. 正确理解Widget::Widget(QWidget *parent) :QWidget(parent)这句话(初始化列表中无法直接初始化基类的数据成员,所以你需要在列表中指定基类的构造函数)

    最近有点忙,先发一篇我公众号的文章,以下是原文. /********原文********/ 最近很多学习Qt的小伙伴在我的微信公众号私信我,该如何理解下面段代码的第二行QWidget(parent) ...

  2. OOP3(继承中的类作用域/构造函数与拷贝控制/继承与容器)

    当存在继承关系时,派生类的作用域嵌套在其基类的作用域之内.如果一个名字在派生类的作用域内无法正确解析,则编译器将继续在外层的基类作用域中寻找该名字的定义 在编译时进行名字查找: 一个对象.引用或指针的 ...

  3. C++类中函数(构造函数、析构函数、拷贝构造函数、赋值构造函数)

    [1]为什么空类可以创建对象呢? 示例代码如下: #include <iostream> using namespace std; class Empty { }; void main() ...

  4. scala中的面向对象定义类,构造函数,继承

    我们知道scala中一切皆为对象,函数也是对象,数字也是对象,它是一个比java还要面向对象的语言. 定义scala的简单类 class Point (val x:Int, val y:Int) 上面 ...

  5. scala入门教程:scala中的面向对象定义类,构造函数,继承

    我们知道scala中一切皆为对象,函数也是对象,数字也是对象,它是一个比java还要面向对象的语言. 定义scala的简单类 class Point (val x:Int, val y:Int) 上面 ...

  6. C++-什么时候需要在类的构造函数中使用初始化列表

    1,如果基类没有default构造函数,则意味着其不能自己初始化.如果其被派生,派生类的构造函数要负责调用基类的构造函数,并传递给它需要的参数.下例中Base 2,如果类成员没有默认构造函数.下例中E ...

  7. ThinkPHP中的__initialize()和类的构造函数__construct()

    ThinkPHP中的__initialize()和类的构造函数__construct()网上有很多关于__initialize()的说法和用法,总感觉不对头,所以自己测试了一下.将结果和大家分享.不对 ...

  8. 【C++】类中this指针的理解

    转自 苦涩的茶https://www.cnblogs.com/liushui-sky/p/5802981.html C++类中this指针的理解 先要理解class的意思.class应该理解为一种类型 ...

  9. Java中Class和单例类的作用与类成员的理解

    Java中Class类的作用与深入理解 在程序运行期间,Java运行时系统始终为所有的对象维护一个被称为运行时的类型标识.这个信息跟踪着每个对象所属的类.JVM利用运行时信息选择相应的方法执行.而保存 ...

随机推荐

  1. JVM垃圾回收机制总结(5) :JDK垃圾收集器的配置命令

    以下配置主要针对分代垃圾回收算法而言. 堆大小设置 年轻代的设置很关键 JVM中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制:系统的可用虚拟内存限制:系统的可用物理 ...

  2. Linux系统VIM编辑器

    vim,linux系统中一款超好用的文本编辑器,是vi的升级版. 三种操作模式 命令模式: 控制光标移动,可对文本进行删除.恢复.黏贴等工作 输入模式: 正常的文本录入 末行模式: 保存,退出与设置编 ...

  3. linux select

    man select: #include <sys/select.h> #include <sys/time.h> int select(int nfds, fd_set *r ...

  4. cmd执行mssql脚本或者执行mysql脚本

    private static int ExecuteMSSql(DbInfo db, string sqlPath) { Console.WriteLine("=============== ...

  5. Java I/O操作学习笔记

    书上写的有点乱,所以就自己总结了一下,主要参考:http://www.cnblogs.com/qianbi/p/3378466.html 1.从文件读出和写入: import java.io.*; i ...

  6. CollectionBase类

    在命名空间System.Collections下的CollectionBase类 The CollectionBase class exposes the interfaces IEnumerable ...

  7. Spring Transaction + MyBatis SqlSession事务管理机制[marked]

  8. Itext导出PDF,word,图片案例

    iText导出pdf.word.图片 一.前言 在企业的信息系统中,报表处理一直占比较重要的作用,本文将介绍一种生成PDF报表的Java组件--iText.通过在服务器端使用Jsp或JavaBean生 ...

  9. 用vi修改文件,保存文件时,提示“readonly option is set”的解决方法

    来源:http://superuser.com/questions/300500/ubuntu-unable-to-edit-bashrc-file-because-of-readonly This ...

  10. R语言实战读书笔记(四)基本数据管理

    4.2 创建新变量 几个运算符: ^或**:求幂 x%%y:求余 x%/%y:整数除 4.3 变量的重编码 with(): within():可以修改数据框 4.4 变量重命名 包reshape中有个 ...