C++ 构造函数详解

article/2025/9/14 19:45:07

目录

0. 什么是构造函数

1. 默认构造函数

2. 一般构造函数

3. 拷贝构造函数

4. 转换构造函数

5. 移动构造函数


0. 什么是构造函数

在定义类的成员函数的时候,一般的成员函数与普通的全局函数没有太大的区别,在定义函数的时候都需要说明要返回的类型,比如:

int fun() {};  //返回int类型
void fun() {};  //什么都不返回

但是,类中有一种函数比较特殊,函数名与类名相同,且没有返回值类型,这中函数称为构造函数,它承担着类初始化的工作,非常重要。

常用的构造函数有默认构造函数、一般构造函数、拷贝构造函数、转换构造函数、移动构造函数。

1. 默认构造函数

首先要有一个概念,一个类时必有构造函数的,即便程序员没有手动定义构造函数,编译器也会创建一个默认构造函数,具体形式如下:

ClassName(){};

这是一个空的函数,即什么都不做。当然,实际上,默认构造函数也不是空的,这里只是简化一下,不作深究。

还值得注意的是,默认构造函数不止有一个,编译器还会创建其他类型的构造函数,比如,拷贝构造函数,这些后边再说。

所以,现在有一个认识就行,那就是如果程序员不手动定义构造函数,编译器会自动定义默认构造函数,即便这个构造函数可能什么都不做。

2. 一般构造函数

当程序员手动定义构造函数之后,编译器便不再生成默认构造函数。构造函数一般承担初始化工作,比如对成员变量初始化,看个栗子:

#include <iostream>
using namespace std;class Person {
public:int age;char* name;Person(int age, char* name) {this->age = age;this->name = name;}
};int main(){Person p(12,"kang");cout<<p.age<<endl;cout<<p.name<<endl;
}

运行结果:

构造函数支持重载,也就是说,可以有多个同名构造函数,不过他们的形参需要有区别。看栗子:

#include <iostream>
using namespace std;class Person {
public:int age;char* name;Person();Person(int age, char* name);Person(char* name);};Person::Person() {this->age = -1;this->name = "-1";
}Person::Person(int age, char* name) {this->age = age;this->name = name;
}Person::Person(char* name) {this->age = -1;this->name = name;
}int main(){Person p1(12,"kang");cout<<p1.age<<"  "<<p1.name<<endl;Person p2;cout<<p2.age<<"  "<<p2.name<<endl;Person p3("kang");cout<<p3.age<<"  "<<p3.name<<endl;
}

运行结果:

3. 拷贝构造函数

类在实例化的时候,有两步操作:

  1. 给对象分配内存,此时内存没有经过初始化,成员变量大多都是垃圾值;
  2. 对内存进行初始化,也就是调用构造函数对成员变量进行初始化。

对于一般构造函数,初始化的值来自于构造函数的输入参数,如果某个构造函数的输入参数是同类的对象,则称该构造函数为拷贝构造函数。

说白了,拷贝构造函数就是用别的对象来初始化新对象的内存。看一个栗子:

#include <iostream>
using namespace std;class Person {
public:int age;char* name;Person(int age, char* name);  //一般构造函数Person(const Person &p);  //拷贝构造函数  };Person::Person(int age, char* name) {this->age = age;this->name = name;
}Person::Person(const Person &p) {this->age = p.age;this->name = p.name;
}int main(){Person p1(12,"kang");   //调用一般构造函数Person p3(14,"wang");Person p2 = p1;      //调用拷贝构造函数,等效于 Person p2(p1);cout<<p1.age<<"  "<<p1.name<<endl;cout<<p2.age<<"  "<<p2.name<<endl;p2 = p3;
}

运行结果:

注意!27行的 Person p2 = p1 语句不是赋值,是在创建对象,因此要调用拷贝构造函数,而31行是对象的赋值,调用的是运算符“=”的重载函数。所以要注意,这俩东西调用是不一样的,类似与变量创建和赋值的区别。

如果程序员没有手动创建拷贝构造函数,编译器会自动创建一个默认拷贝构造函数,如果手动创建了,编译器就不会再创建了。默认拷贝构造函数很简单,就是用老对象”的成员变量对“新对象”的成员变量进行一一赋值,和上面 Person 类的拷贝构造函数非常类似。所以,上边那个例子,不定义拷贝构造函数,程序也是没问题的,因为编译器帮我们创建了一个默认构造函数。

可能会有疑问,既然编译器自己会创建拷贝构造函数,那为什么还需要用户手动创建呢?答案是,默认构造函数里边只是简单的浅拷贝,碰到指针就G了。对于指针指向的数据,如果用默认构造函数,那么“新对象”的指针跟“老对象”的指针指的是同一块内存,”老对象“的修改会影响到“新对象”,所以这时候就需要用户手动创建拷贝构造函数,使用深拷贝创建对象。

4. 转换构造函数

对于类型转换,大家可能并不陌生,C++自带很多类型转换规则,比如int转double,(int*)转(float*),以及向上型转等等,除了这些自带的转换规则之外,用户还可以自定义转换规则,转换构造函数就是将其他类型转化为当前类。

借助前边的例子,如果我的对象创建语句是这么写的:

Person p = "Kang";

这个代码是编译不过的,因为Person类没有对应的构造函数,但是如果定义一个转换构造函数,自定义一下转换规则,那么这条语句就是可行的,看一个详细的例子:

#include <iostream>
#include <string.h>
using namespace std;class Person {
public:int age;char* name;Person(int age, char* name);  //一般构造函数Person(char* name);  //转换构造函数};Person::Person(int age, char* name) {this->age = age;this->name = name;
}Person::Person(char* name) {  //转换构造函数this->name = name;this->age = 0;
}int main(){Person p1(12,"kang");   //调用一般构造函数Person p2 = "Tom";      //调用转换构造函数,等效于 Person p2(“Tom”);cout<<p1.age<<"  "<<p1.name<<endl;cout<<p2.age<<"  "<<p2.name<<endl;p2 = "Jack";  //调用转换构造函数,因为=右边不是Person类型,所以不会调用运算符=的重载函数cout<<p2.age<<"  "<<p2.name<<endl;
}

运行结果:

 可以看到,Person p2 = "Tom" 是没有问题的,正常来说,"Tom"是一个字符串,无法用他对 p2 进行初始化的,但是编译器会判断类型转换规则能不能用,再查看类中是否有转换构造函数,然后将“Tom”通过转换构造函数转化为Person类的对象,然后再调用拷贝构造函数对p2进行初始化。相当于中间用转换构造函数创建了一次匿名类,然后再使用拷贝构造函数。不得不说,编译器真的很努力了~

对于 p2 = "Jack" ,则先调用转换构造函数,再调用了赋值运算符重载函数。

需要强调的是,转换构造函数用于标准类型向自定义类转换,因此转换构造函数的形参只能有一个,或者有多个形参但最多只有一个不是默认参数。

Person::Person(char* name) {  //转换构造函数this->name = name;this->age = 0;
}Person::Person(char* name, int age = 0) {  //转换构造函数this->name = name;this->age = age;
}Person::Person(int age, char* name) {  //不是转换构造函数this->age = age;this->name = name;
}

5. 移动构造函数

拷贝构造函数可以用其他对象初始化新对象,但是,如果成员指针变量指向的数据量很大,再进行深拷贝,那么拷贝函数的时间开销是巨大的。而移动构造函数的诞生,就是为了解决拷贝构造函数时间开销大的问题。

借助一个例子来说明移动构造函数是如何工作的:

Person p = Person();

这条语句会先调用一般构造函数或者默认构造函数(取决于有没有手动定义一般构造函数),创建一个匿名对象;再调用拷贝构造函数。如果Person成员指针变量指向的数据量很大,那么时间开销是很大的。

移动构造函数的优化思路是:既然一般构造函数创建出来的是一个匿名对象,我们不妨把新对象的指针直接指向匿名对象指向的空间,说简单一些就是,指针简单拷贝,然后将匿名对象的指针置空,因为匿名对象是无法被用户调用的,如此一来,匿名对象成员指针指向的数据都归新对象的指针变量了。有点鸠占鹊巢的意思==>

移动构造函数的声明形式如下:

ClassName(CalssNmae &&obj);

可以看到,为了能够引用匿名对象,移动构造函数的输入是右值引用。

光说不直观,看一个例子:

#include <iostream>
#include <string.h>
#include <chrono>
using namespace std;class demo{
public:demo(){               //一般构造函数num = new int[100000000];cout<<"constructor"<<endl;}demo(const demo& de) {   //拷贝构造函数num = new int[100000000];memcpy(this->num, de.num, 100000000);cout<<"copy constructor"<<endl;}
//    demo(demo &&de) {     //移动构造函数
//        num = de.num;
//        de.num = NULL;
//        cout<<"move constructor"<<endl;
//    }
private:int *num;
};demo get_demo(){return demo();
}int main(){auto start = std::chrono::system_clock::now();demo a = demo();auto finish = std::chrono::system_clock::now();auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(finish-start);std::cout<<double(duration.count())<< "ms" <<std::endl;return 0;
}

这个代码不能用IDE去编译运行,因为IDE会进行优化,我们就无法观察程序原本的运行轨迹了。

在Linux系统下,找到cpp所在文件夹,打开终端,输入:

g++ main.cpp  -fno-elide-constructors -o main

其中,g++是选择的编译器,main.cpp是源文件,-fno-elide-constructors是禁用构造函数优化。然后,当前目录下会生成一个名为main的可执行程序,运行便可得到程序输出。

运行结果:

 可以看到,拷贝构造函数为了复制长度100000000的数组,运行了35ms。

现在我们将17-21行的注释去掉,也就是定义了移动构造函数,再次编译运行,结果为:

 可以看到,没有了大块的内存拷贝,运行时间已经不在ms量级了(大约0.1ms),这样就大大降低了拷贝构造函数的时间开销。

从这个代码也能看出来:

当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。


http://chatgpt.dhexx.cn/article/ENuFB5je.shtml

相关文章

【C++要笑着学】类的默认成员函数详解 | 构造函数 | 析构函数 | 构造拷贝函数

&#x1f525; &#x1f525; &#x1f525; &#x1f525; &#x1f525; 火速猛戳订阅 &#x1f449; 《C要笑着学》 &#x1f448; 趣味教学博客 &#x1f525; &#x1f525; &#x1f525; &#x1f525; &#x1f525; [ 本篇博客热榜最高排名&#xff1a;7 ] 写在前…

JSF 文档参考

转自&#xff1a;http://blog.csdn.net/ontheway20/article/details/38532241 A4J 用户指南 目录 1. 介绍 2. 开始使用Ajax4jsf 环境需求 下载Ajax4jsf 安装 简单的 AJAX Echo 项目 JSP 页面 数据 Bean faces-config.xml Web.xml 部署 3. Ajax4jsf 框架的基本概念 介绍 结构概…

IDEA 2020.2 部署JSF项目

目录 一、用Glassfish部署JSF项目 1、下载glassfish 2、配置glassfish环境变量 3、修改jdk环境变量 4、测试glassfish是否可以正常启动 5、在IDEA中创建一个JSF项目 6.问题&#xff1a;部分标签元素无法显示 二、用tomcat部署JSF项目 1、新建项目或者模块&#xff0c;…

JSF教程(1)——简介 + HelloWorld

在写第一个HelloWorld之前先来宏观的了解一下JSF&#xff0c;也许你之前使用过Struts&#xff08;1或者2&#xff09;&#xff0c;SpringMVC&#xff0c;甚至于直接采用JSPServelet开发过web层。JSF与这些最大的不同是JSF是基于一种以组件为中心的用户界面&#xff08;UI&#…

JSF程式

概述&#xff1a; jsf使用spring的依赖注入的思想使得页面和业务逻辑更好的分离开来&#xff0c;页面与页面的跳转&#xff0c;逻辑关系&#xff0c;页面与后台不同的beans的对应和操作都是通过faces-config.xml文件来说明和配置。对程序员的要求不高&#xff0c;页面程序员可…

谈谈京东的服务框架JSF

谈谈京东的服务框架 最近由于在实习期间接触到了京东的自研服务框架JSF&#xff0c;简称“杰夫”&#xff0c;目前我写的一些新功能里面调用的下游接口就是杰夫提供的。现有有很多高效的服务框架&#xff0c;如阿里巴巴的Dubbo配合Apache的ZooKeeper&#xff0c;那么为什么京东…

JSF 转换与验证

在本文中&#xff0c;我们将介绍 JSF 转换和验证框架的概念&#xff0c;它比您所想的要容易使用得多&#xff0c;也灵活得多。 首先我们将介绍应用于 JSF 生命周期的转换和验证过程&#xff0c;然后展示一个简单的 JSF 应用程序中的默认转换和验证过程。接着将展示如何创建和插…

JSF----------基础知识初解

初次学习JSF,对其基础进行了一些学习与整理。 JSF(JavaServer Faces)它是一个基于服务器端组件的用户界面框架。 它用于开发Web应用程序。 它提供了一个定义良好的编程模型&#xff0c;由丰富的API和标签库组成。最新版本JSF 2使用Facelets作为其默认模板系统。 它是用Java编写…

JSF详解

1&#xff0e; 结构&#xff1a; a) 结构图&#xff1a; b) 说明&#xff1a;JSF以MVC模式为基础&#xff0c;与Struts不同&#xff0c;JSF的目标是希望以一个与Swing相类似的方式来开发网页&#xff0c;因此&#xff0c;从JSF的结构图当中&#xff0c;他的核心…

JSF框架整理(一)

一、框架简介 JavaServer Faces (JSF) 是一种用于构建Java Web 应用程序的标准框架&#xff0c;它提供了一种以组件为中心的用户界面&#xff08;UI&#xff09;构建方法&#xff0c;从而简化了Java服务器端应用程序的开发。 典型的JSF应用程序包含下列部分&#xff1a; 一组J…

JSF简介

JSF简介 一、 什么是 JSF &#xff1a; JavaServer Faces (JSF) 是一种用于构建 Web 应用程序的新标准 Java 框架。它提供了一种以组件为中心来开发 Java Web 用户界面的方法&#xff0c;从而简化了开发。 JavaServer Faces于2004年三月1.0版正式提出&#xff0c;清楚的将Web应…

JSF概述

1. JSF简洁 JSF是一种以组件为中心&#xff0c;遵循MVC设计模式的一种框架。 Web引用程序开发人员划分&#xff1a;网页设计人员应用程序设计人员UI组件设计人员 所有与应用程序都由一个前端控制器(FacesServlet)来处理 2. JSF声明周期 FacesServlet充当用户和JSF应用程序之间的…

ztree项目

思路&#xff1a; 创建一个登陆 登陆上去 就是树 每个是的根节点有他所要展示的内容 表 可以有无数个 主要说的是创建树的表 这个是树的一个表 id 是 节点 name 名字 pid 根节点 url 路径 树的页面 后台通过登陆转的页面 转页面 在前台打印出你想要的数据 前台页面 退出 /*…

ztree使用

官方文档地址 http://www.treejs.cn/v3/main.php#_zTreeInfo 各种参数 http://www.treejs.cn/v3/api.php 简单静态调用 <!DOCTYPE html> <html><head><meta charset"utf-8"><title></title><link href"https://cdn…

zTree 简介

zTree 是一个依靠 jQuery 实现的多功能 “树插件”。优异的性能、灵活的配置、多种功能的组合是 zTree 最大优点。 zTree 是开源免费的软件&#xff08;MIT 许可证&#xff09;。如果您对 zTree 感兴趣或者愿意资助 zTree 继续发展下去&#xff0c;可以进行捐助。 zTree v3.0 …

ztree 详解

官网:http://www.treejs.cn/v3/demo.php#_101 里面有例子和demo,齐全。 zTree是一个基于jQuery的树形列表生成控件。 切换语言,点击下载,里面有各种demo: 里面各种demo,比博客写的好。 <!DOCTYPE html> <html lang="en"><head><meta ch…

zTree的简单使用1.0

2018/10/10 北京朝阳.冠城大厦17楼 这里是引用 zTree 简介 zTree 是一个依靠 jQuery 实现的多功能 “树插件”。优异的性能、灵活的配置、多种功能的组合是 zTree 最大优点。 zTree 是开源免费的软件&#xff08;MIT 许可证&#xff09;。如果您对 zTree 感兴趣或者愿意资助 zT…

ZTree基本使用及本人详解

文章目录 ZTree树简介简介ZTree的特点练习ztree之前的小建议ZTree文件介绍ZTree的配置介绍 ZTree使用案例需求1&#xff1a;前端初始化数据&#xff08;标准json数据&#xff09;前端代码 需求2&#xff1a;后端查询ztree数据&#xff08;简单JSON数据&#xff09;需求3&#x…

ztree 使用教程

zTree 是一个依靠 jQuery 实现的多功能 “树插件”。被广泛应用在系统的权限管理中&#xff0c;本文讲解zTree的一般应用 zTree 官网 http://www.treejs.cn/v3/main.php#_zTreeInfo 1、zTree 官网下载 ztree 下载好后放到项目相关目录下 2、编写相关代码 引入相关js 、 css …

@PersistenceContext和@Autowired在EntityManager上应用的不同

首先PersistenceContext是jpa专有的注解&#xff0c;而Autowired是spring自带的注释 上方图片的意思就是EntityManager不是线程安全的&#xff0c;当多个请求进来的时候&#xff0c;spring会创建多个线程&#xff0c;而PersistenceContext就是用来为每个线程创建一个EntityMana…