1. 引述

在C++中,学习类的第一课往往就是构造函数。根据构造函数的定义,构造函数式是用于初始化类对象的数据成员的。无论何时,只要类被创建,就会执行构造函数:

class ImageEx
{
public:
ImageEx()
{
cout << "Execute the constructor!" << endl;
}
}; int main()
{
ImageEx imageEx;
return 0;
}

那么问题来了,为什么要有构造函数?

2. 详述

2.1. 数据类型初始化

正如上一篇文章《面向对象编程(C++篇1)——引言》中提到的那样:类是抽象的自定义数据类型。对于C++的内置数据类型,我们可以采用如下方式进行初始化:

double price = 109.99;

这种初始化行为很像赋值操作,但是初始化与赋值是两种概念:初始化的含义是创建变量的时候赋予其一个初始值,而赋值的含义则是把对象的当前值擦除,以一个新的值来代替。实际上,我们同样可以使用类似构造函数一样的方式初始化内置数据类型:

double price(109.99);

那么,我们在定义变量的时候不进行初始化会怎么样呢?答案是会进行默认初始化(其实不太准确,在某些情况下,会不被初始化,进而产生未定义的行为,是非常危险的):

double price;
price = 109.99;

在C++中,一个合理的原则是:变量类型定义时初始化。这个原则不仅可以避免未初始化可能产生的未定义行为,还节省了性能:避免定义(默认初始化)后再进行赋值操作。

2.2. 类初始化

可能你会认为,先定义(默认初始化)之后再进行赋值,对性能影响不大。这句话对于C#、Java、JavaScript这样的语言来说是成立的,它们的应用场景很多时候可以不用关心这个(性能场景则不一定)。而对于C++这样的面向底层的语言来说,追求的是"零成本抽象(zero overhead abstraction)"的设计原则,只是简单的数据结构影响当然不太,但是对于一个非常复杂的数据类型,则可能存在不可忽视的性能开销。

可以为一个类的数据成员提供一个类内初始值:

class ImageEx
{
int imgWidth = 0;
int imgHeight = 0;
int bandCount = 0;
};

类的数据成员如果不进行初始化,那么就会如前所述,进行默认初始化:

class ImageEx
{
public: void Print()
{
cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
for (int i = 0; i < 10; i++)
{
printf("%d\t", data[i]);
}
} private:
int imgWidth;
int imgHeight;
int bandCount; unsigned char data[10];
}; int main()
{
ImageEx imageEx;
imageEx.Print(); return 0;
}

运行结果:

默认初始化的未定义行为当然不是我们想要的,于是我们给他加一个初始化函数:

class ImageEx
{
public: void Init()
{
imgWidth = 200;
imgHeight = 100;
bandCount = 3;
memset(data, 0, 10 * sizeof(unsigned char));
} void Print()
{
cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
for (int i = 0; i < 10; i++)
{
printf("%d\t", data[i]);
}
cout << endl;
} private:
int imgWidth;
int imgHeight;
int bandCount; unsigned char data[10];
}; int main()
{
ImageEx imageEx;
imageEx.Print();
imageEx.Init();
imageEx.Print(); return 0;
}

运行结果:

从上例可以发现,如果我们自己给类的数据成员进行初始化函数,其实类的数据成员早就进行了一次默认初始化操作,这个初始化函数其实是一次额外的赋值。以这个类对象中的数组数据成员data为例,假使这个数组的容量很大,其额外的一次赋值操作对于底层来说,是不可忽略的性能开销。

那么使用构造函数的原因就很容易理解了,构造函数就是实现当类定义时初始化数据成员的,这样可以避免额外的初始化性能开销:

class ImageEx
{
public:
ImageEx()
{
cout << "Default initialization!" << endl;
Print();
cout << "Execute the constructor!" << endl;
Init();
} void Print()
{
cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
for (int i = 0; i < 10; i++)
{
printf("%d\t", data[i]);
}
cout << endl;
} private:
void Init()
{
imgWidth = 200;
imgHeight = 100;
bandCount = 3;
memset(data, 0, 10 * sizeof(unsigned char));
} int imgWidth;
int imgHeight;
int bandCount; unsigned char data[10];
}; int main()
{
ImageEx imageEx;
imageEx.Print(); return 0;
}

进一步探究,构造函数本质是个函数,函数是由语句组成,已经定义的数据类型只能赋值初始化,而无法再进行构造。也就是说,在调用构造函数之前,数据成员还是已经默认初始化了:

因此,初始化最好的实现是使用构造函数的初始值列表:

class ImageEx
{
public:
ImageEx() :
imgWidth(200),
imgHeight(100),
bandCount(3),
data{ 0, 1, 2 }
{
cout << "Execute the constructor!" << endl;
} void Print()
{
cout << imgWidth << '\t' << imgHeight << '\t' << bandCount << endl;
for (int i = 0; i < 10; i++)
{
printf("%d\t", data[i]);
}
cout << endl;
} private:
int imgWidth;
int imgHeight;
int bandCount; unsigned char data[10];
}; int main()
{
ImageEx imageEx;
imageEx.Print(); return 0;
}

运行结果:

通过这种实现,类中所有的数据成员都在定义时初始化,从而使类对象也实现了定义时初始化;避免了先定义后赋值的性能开销,体现了C++"零成本抽象(zero overhead abstraction)"的设计哲学。

上一篇

目录

下一篇

面向对象编程(C++篇2)——构造的更多相关文章

  1. 面向对象编程(C++篇1)——引言

    目录 1. 概述 2. 详论 2.1. 类与对象 2.2. 数据类型 3. 目录 1. 概述 现代C++与最原始的版本已经差不多是两种不同的语言了.不断发展的C++标准给C++这门语言带来了更多的范式 ...

  2. 面向对象编程(C++篇4)——RAII

    目录 1. 概述 2. 详论 2.1. 堆.栈.静态区 2.2. 手动管理资源的弊端 2.3. 间接使用 2.4. 自下而上的抽象 3. 总结 4. 参考 1. 概述 在前面两篇文章<面向对象编 ...

  3. Python 第六篇(中):面向对象编程中级篇

    面向对象编程中级篇: 编程思想概述: 面向过程:根据业务逻辑从上到下写垒代码  #最low,淘汰 函数式:将某功能代码封装到函数中,日后便无需重复编写,仅调用函数即可 #混口饭吃 def add(ho ...

  4. 面向对象编程(C++篇3)——析构

    目录 1. 概述 2. 详论 2.1. 对象生命周期 2.2. 不一定需要显式析构 2.3. 析构的必要性 3. 总结 1. 概述 类的析构函数执行与构造函数相反的操作,当对象结束其生命周期,程序就会 ...

  5. python - 面向对象编程(初级篇)

    写了这么多python 代码,也常用的类和对象,这里准备系统的对python的面向对象编程做以下介绍. 面向对象编程(Object Oriented Programming,OOP,面向对象程序设计) ...

  6. Python 第六篇(上):面向对象编程初级篇

    面向:过程.函数.对象: 面向过程:根据业务逻辑从上到下写垒代码! 面向过程的编程弊:每次调用的时候都的重写,代码特别长,代码重用性没有,每次增加新功能所有的代码都的修改!那有什么办法解决上面出现的弊 ...

  7. 面向对象编程-终结篇 es6新增语法

    各位,各位,终于把js完成了一个段落了,这次的章节一过我还没确定下面要学的内容可能是vue也可能是前后端交互,但无论是哪个都挺兴奋的,因为面临着终于可以做点看得过去的大点的案例项目了,先憋住激动地情绪 ...

  8. 洗礼灵魂,修炼python(41)--巩固篇—从游戏《绝地求生-大逃杀》中回顾面向对象编程

    声明:本篇文章仅仅以游戏<绝地求生>作为一个参考话题来介绍面向对象编程,只是作为学术引用,其制作的非常简易的程序也不会作为商业用途,与蓝洞公司无关. <绝地求生>最近很火,笼络 ...

  9. Python(三)基础篇之「模块&面向对象编程」

    [笔记]Python(三)基础篇之「模块&面向对象编程」 2016-12-07 ZOE    编程之魅  Python Notes: ★ 如果你是第一次阅读,推荐先浏览:[重要公告]文章更新. ...

随机推荐

  1. How to check in Windows if you are using UEFI

    You might be wondering if Windows is using UEFI or the legacy BIOS, it's easy to check. Just fire up ...

  2. 计算机电子书 2016 BiliDrive 备份

    下载方式 根据你的操作系统下载不同的 BiliDrive 二进制. 执行: bilidrive download <link> 链接 文档 链接 Go入门指南.epub (1.87 MB) ...

  3. Lesson3——NumPy 数据类型

    NumPy 教程目录 NumPy 数据类型 numpy 支持的数据类型比 Python 内置的类型要多很多,基本上可以和 C 语言的数据类型对应上,其中部分类型对应为 Python 内置的类型. 下表 ...

  4. Ansible 自动化运维——剧本(playbook)

    Ansible 自动化运维--剧本(playbook) 1.playbook介绍: playbook是ansible用于配置,部署,和管理被控节点的剧本.通过playbook的详细描述,执行其中的ta ...

  5. Shell双重循环、图形排列及九九乘法表

    Shell双重循环.图形排列及九九乘法表 目录 Shell双重循环.图形排列及九九乘法表 一.双重循环 1. 双重循环概述 2. 双重循环结构 二.循环特殊操作 1. exit 2. break 3. ...

  6. Linux基础入门笔记

    今天带来Linux入门的一些基础的笔记,科班出身的同学们,Linux已经成为了必修课了,下面我带来关于Linux的相关入门知识以及Linux简单的介绍! Linux内核最初只是由芬兰人林纳斯·托瓦兹( ...

  7. 基于GDAL库海洋表温日平均计算工具设计与实现 C++版

    技术背景 在对物理海洋数据处理过程中,表层温度是众多要素中的一种,本文书要是针对海洋表温数据批量日平均处理的一个工具设计.首先要在对当前的SST数据文件作一下简要的说明,SST全称为sea surfe ...

  8. v78.01 鸿蒙内核源码分析(消息映射篇) | 剖析LiteIpc(下)进程通讯机制 | 百篇博客分析OpenHarmony源码

    百篇博客分析|本篇为:(消息映射篇) | 剖析LiteIpc(下)进程通讯机制 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析( ...

  9. NPM 错误、问题等汇总

    一. npm的作用就是对Node.js依赖的包进行管理,也可以理解为用来安装/卸载Node.js需要装的东西 二. 1. 修改npm配置为淘宝的源下载: npm install -g cnpm --r ...

  10. Spring Boot数据访问之数据源自动配置

    Spring Boot提供自动配置的数据访问,首先体验下,Spring Boot使用2.5.5版本: 1)导入坐标: 2.5.25版本支持8.0.26mysql数据库驱动.spring-boot-st ...