C++对象之谜(封装篇)
这篇博客简要记录下C++对象的相关内容,以便回顾时使用。
C++类的定义
我们使用C++定义一个矩形(Rectangle)类,它的基本属性有:长(width),宽(width), 对矩形的基本操作有:计算其周长(circumference), 计算其面积(area). 矩形类的定义如下:
class Rectangle {
public:
Rectangle(unsigned int width, unsigned int height) {
m_width = width;
m_height = height;
}; // 构造函数
~Rectangle() = default; // 析构函数
unsigned int get_circumference();
unsigned int get_area();
private:
unsigned int m_width; // 矩形的宽
unsigned int m_height; // 矩形的高
};
unsigned int Rectangle::get_circumference() {
return 2 * (m_width + m_height);
}
unsigned int Rectangle::get_area() {
return m_width * m_height;
}
一般来说,类由一系列数据以及对数据的一系列操作构成,我们希望隐藏数据,而提供对数据的操作。C++的public和private正起着这样的作用,使用public修饰的成员可以被外界访问,而使用private修饰的成员不可以被外界访问。
小提示:有的人习惯将类的成员变量名以
m_开头,这样写看起来比较直观,但这也只是一种编程风格,我们使用了这样的风格,例如m_width和m_height.
对于类中的方法,可以将方法的具体实现直接写在类的声明中,例如这里的Rectangle(unsigned int width, unsigned int height); 也可以在类的声明外另写,不过此时需要在方法名的前面加上类的名称后跟两个冒号,例如这里的Rectangle::get_area, 表示这个方法是类Rectangle中的方法。
构造函数
如何根据类的声明创建一个对象(或者说这个类的一个实例)?这就是构造函数需要做的事情。构造函数是一个特殊的函数,在对象被创建时调用一次,通常用于初始化类中的数据成员或者做一些其它初始化的工作,它没有返回值,也没有返回值类型。它的声明由类名和参数列表构成:
ObjectName(type1 param1, type2 param2, ...);
例如这里的Rectangle(unsigned int width, unsigned int height).
可以定义多个构造函数,只要它们的参数列表不同,在调用时编译器会选择合适的构造函数。
析构函数
对象在销毁之前,会调用一次析构函数。因为有的对象可能占用了某些资源,例如占用了堆中的一部分空间(在销毁前应当释放空间)、打开了文件(在销毁前应当关闭文件)等等,或是完成其它的工作。它在对象被销毁前调用一次,它的声明通常由一个波浪号~和对象名构成,而且析构函数没有参数:
~ObjectName();
例如这里的~Rectangle(). 在C++中,如果某些特殊方法没有被定义,例如构造函数和析构函数,C++编译器会为这些特殊方法创建一个默认的版本。这里定义的矩形类很简单,在对象销毁前并不需要做额外的操作,因此可以直接使用默认的版本。为了使用C++编译器创建的默认版本,你可以什么都不写,或是像这里的写法:
~Rectangle() = default;
在后面加上 = default 从而告诉编译器我们将使用默认的版本。
只要你实现了一个构造函数,
C++编译器就不会创建默认的构造函数。如果你想要保留这个由编译器创建的、不带参数的构造函数,那么就使用= default.
创建第一个对象
好了,现在可以创建一个矩形对象。在此之前,我们更改下矩形类的实现,在调用构造函数和析构函数时,输出此函数被调用的语句:
// 其它部分保持不变
Rectangle(unsigned int width, unsigned int height) {
std::cout << "调用构造函数\n";
m_width = width;
m_height = height;
}; // 构造函数
~Rectangle() {
std::cout << "调用析构函数\n";
}; // 析构函数
现在,在main()中创建一个矩形对象:
int main() {
Rectangle rect(2, 3);
std::cout << "矩形的面积为" << rect.get_area() << std::endl;
}
你会看到这样的输出结果:
调用构造函数
矩形的面积为6
调用析构函数
看到了吧,对象被创建时会调用构造函数,对象被销毁时会调用析构函数。
拷贝构造函数
在C++中,可以根据一个已经存在的对象构造一个新的对象,例如:
Rectangle rect1(2, 3);
Rectangle rect2(rect1);
Rectangle rect3 = rect1;
可以利用rect1构造rect2, 这是通过拷贝构造函数实现的。
拷贝构造函数的声明如下:
ObjectName(const ObjectName& obj);
例如,矩形类的拷贝构造函数声明如下:
Rectangle(const Rectangle& rect);
可能你已经看出来了,从形式上来看,它和拷贝函数的形式是一样的,只不过参数是一个矩形对象罢了。通常来说,对象通过引用传递更高效(接下来我们将会看到),而且从概念上来说,一个拷贝构造函数不应当修改传入的参数(也就是用于构造新对象的原对象),所以这里的参数有&和const修饰。
我们为矩阵类的拷贝构造函数添加具体实现:
Rectangle::Rectangle(const Rectangle& rect) {
std::cout << "调用拷贝构造函数\n";
m_width = rect.m_width;
m_height = rect.m_height;
}
下面用一个例子看一下拷贝构造函数何时被调用:
int main() {
Rectangle rect1(2, 3);
std::cout << "矩形1的面积为" << rect1.get_area() << std::endl;
std::cout << "\n";
Rectangle rect2(rect1);
std::cout << "矩形2的面积为" << rect2.get_area() << std::endl;
std::cout << "\n";
}
输出为:
调用构造函数
矩形1的面积为6
调用拷贝构造函数
矩形2的面积为6
调用析构函数
调用析构函数
总之一句话,将拷贝构造函数看作构造函数的一种,只不过这种函数比较特殊,我们单独拿出来强调下而已。
小提示:如果你需要实现自己的析构函数或是拷贝构造函数,那么绝大多数情况下你需要同时实现这两个,至于其中的原因,应该很容易想到。
赋值运算符重载
下面的这种写法也很常见:
Rectangle rect1(2, 3);
Rectangle rect2(1, 1);
rect2 = rect1;
要想支持这种写法,必须实现用于运算符重载的函数,这里我们需要重载赋值运算符,也就是=, 对于矩形类,其声明如下:
Rectangle& operator=(const Rectangle& rect);
这里,返回值的类型为Rectangle&是为了支持多重赋值,也就是rect3 = rect2 = rect1这样的写法。下面给出此函数的一个实现:
Rectangle& operator=(const Rectangle& rect) {
std::cout << "调用赋值运算符重载函数\n";
m_width = rect.m_width;
m_height = rect.m_height;
return *this;
}
这里出现了一个新的关键词this. this是一个指向当前对象的指针,例如this->m_width表示使用此对象的m_width成员。因此,*this自然就是this指向的对象本身,这也是C++为我们提供的引用对象自身的方法。
好了,现在我们写一个用例测试下:
int main() {
Rectangle rect1(2, 3);
Rectangle rect2(1, 1);
rect2 = rect1;
}
这个例子的输出为:
调用构造函数
调用构造函数
调用赋值运算符重载函数
调用析构函数
调用析构函数
小提示:
Rectangle rect2 = rect1;没有调用赋值运算符重载函数,而是直接调用拷贝构造函数,相当于Rectangle rect2(rect1);. 由于这里的rect2此时刚被定义,如果有两个选择:(1)直接调用拷贝构造函数,通过rect1构造rect2;(2)首先调用构造函数构造rect2, 再调用赋值运算符重载函数通过rect1重新设置rect2. 你会怎么选?
禁止复制
如果你不希望创造的对象被复制,可以在拷贝构造函数和赋值运算符重载的声明中添加= delete, 表示你想要删除这个函数。例如:
Rectangle(const Rectangle& rect) = delete;
Rectangle& operator=(const Rectangle& rect) = delete;
从而,任何尝试调用此函数的行为,都会引发编译器报错。
慎用友元
正常情况下,外界无法访问对象的private成员,但是,如果被类认为是朋友(friend)的外部对象或是函数是可以访问private成员的。比如下面这个函数:
void print_size(const Rectangle& rect) {
std::cout << "Rectangle(" << rect.m_width << ", " << rect.m_height << ")\n";
}
它会访问矩形对象的m_width和m_height成员,C++编译器是不会允许这种情况的,因此会编译失败,但是,如果我们将函数print_size声明为友元,使用friend关键词,就可以了。在Rectangle类的定义中添加如下声明:
friend void print_size(const Rectangle& rect);
但是,这种行为会破坏数据的封装,使用时一定要谨慎!
小坑:小心潜在的类型转换
正方形也是长方形的一种,但正方形的长和宽相同,因此构造函数只需要一个参数即可,具体实现如下:
Rectangle::Rectangle(unsigned int width) {
std::cout << "调用构造函数[正方形]\n";
m_width = m_height = width;
}
现在再看以下代码:
int main() {
print_size(1);
}
这段代码的输出为:
调用构造函数[正方形]
Rectangle(1, 1)
调用析构函数
什么情况?print_size接收的不是一个Rectangle对象吗?这里传入的是1, 为什么也可以?通过输出我们可以看到,C++通过调用我们刚才定义的用于初始化正方形的构造函数,将1转化为Rectangle(1), 这里发生了隐式的类型转换。
对于具有一个参数的构造函数,都有可能出现上述的情况,可以通过在函数前添加关键词explicit禁止这种类型转换。在这里,也就是:
explicit Rectangle(unsigned int width);
现在重新编译这段代码,编译是不会通过的。
静态成员
简单地说,静态成员是与类绑定的成员,与具体的对象无关。通过关键词static定义静态成员,静态数据成员只能通过由static修饰的方法访问。
使用静态成员可以实现很好玩的事情。例如,记录目前存在多少个矩形对象,具体实现如下:
class Rectangle {
public:
Rectangle(unsigned int width, unsigned int height) {
num_rects += 1;
std::cout << "调用构造函数[矩形]\n";
m_width = width;
m_height = height;
} // 构造函数
explicit Rectangle(unsigned int width);
Rectangle(const Rectangle& rect);
~Rectangle() {
std::cout << "调用析构函数\n"; // 析构函数
num_rects -= 1;
}
static unsigned int get_rects() { return num_rects; };
private:
static unsigned int num_rects; // 目前有多少个矩形对象
unsigned int m_width; // 矩形的宽
unsigned int m_height; // 矩形的高
};
unsigned int Rectangle::num_rects = 0;
这里,我们在矩形类中定义了一个static类型的变量num_rects用于记录创建了矩形对象的数量,在每个构造函数中,将num_rects加1, 且在析构函数中,将num_rects减1. 此外,使用static修饰的方法get_rects返回num_rects的大小。
注意:这里的静态变量num_rects在类的外部初始化,如果有头文件和对应的具体实现文件,那么静态变量应当在具体实现文件中初始化,这是为了防止头文件被多次包含,从而静态变量被多次初始化。不过,在C++17及以后,可以在类的内部初始化静态变量,不过要这样写:
class Rectangle {
private:
static inline unsigned int num_rects = 0;
}
具体可以在网上查查其它资料。
有了静态属性,我们可以在每个矩形对象创建时为每个矩形分配一个id, 这样在输出时可以知道具体是哪个矩形对象输出的。总的实现如下:
class Rectangle {
public:
Rectangle(unsigned int width, unsigned int height) {
m_id = get_id();
print_id();
std::cout << "调用构造函数[矩形]\n";
m_width = width;
m_height = height;
} // 构造函数
explicit Rectangle(unsigned int width) {
m_id = get_id();
print_id();
std::cout << "调用构造函数[正方形]\n";
m_width = m_height = width;
}
Rectangle(const Rectangle& rect) {
m_id = get_id();
print_id();
std::cout << "调用拷贝构造函数\n";
m_width = rect.m_width;
m_height = rect.m_height;
}
~Rectangle() {
print_id();
std::cout << "调用析构函数\n"; // 析构函数
num_rects -= 1;
}
Rectangle& operator=(const Rectangle& rect) {
print_id();
std::cout << "调用赋值运算符重载函数(传入矩形对象的id=" << rect.m_id << ")\n";
m_width = rect.m_width;
m_height = rect.m_height;
return *this;
}
void print_area(){
print_id();
std::cout << "面积为" << m_width * m_height << std::endl;
}
static unsigned int get_rects() { return num_rects; };
private:
static unsigned int num_rects; // 目前有多少个矩形对象
static unsigned int get_id() {
num_rects += 1;
return num_rects;
}
unsigned int m_width; // 矩形的宽
unsigned int m_height; // 矩形的高
unsigned int m_id; // 矩形对象的id
void print_id(){
std::cout << "ID(" << m_id << "): ";
}
};
unsigned int Rectangle::num_rects = 0;
有了这个类,我们就可以探究许多有意思的事情了。
连续赋值
C++中的连续赋值(例如rect1 = rect2 = rect3)具体是怎么工作的?下面这个例子:
int main() {
Rectangle rect1(2, 3), rect2(1), rect3(3);
rect1 = rect2 = rect3;
}
它的输出为:
ID(1): 调用构造函数[矩形]
ID(2): 调用构造函数[正方形]
ID(3): 调用构造函数[正方形]
ID(2): 调用赋值运算符重载函数(传入矩形对象的id=3)
ID(1): 调用赋值运算符重载函数(传入矩形对象的id=2)
ID(3): 调用析构函数
ID(2): 调用析构函数
ID(1): 调用析构函数
从输出可以看出,rect1 = rect2 = rect3具体可以分为以下两步:
rect2.operator=(rect3);
rect1.operator=(rect2);
执行是从右往左进行的,而且表达式rect2 = rect3的返回值是rect2, 从而可以继续执行调用rect1 = rect2, 这也是在赋值运算符重载函数中,需要返回*this的原因。
对象传参
对象作为函数的参数,按值传参与按引用传参有何区别呢?下面我们看一个例子:
void call_rect_by_value(Rectangle rect) {
rect.print_area();
}
在如下的程序中:
int main() {
Rectangle rect(2, 3);
call_rect_by_value(rect);
}
输出为:
ID(1): 调用构造函数[矩形]
ID(2): 调用拷贝构造函数
ID(2): 面积为6
ID(2): 调用析构函数
ID(1): 调用析构函数
可以看出,按值传参时,调用了一次拷贝构造函数。而按引用传递的参数:
void call_rect_by_ref(Rectangle& rect) {
rect.print_area();
}
同样使用上面的测试程序,只不过将call_rect_by_value换成call_rect_by_ref, 输出为:
ID(1): 调用构造函数[矩形]
ID(1): 面积为6
ID(1): 调用析构函数
按引用传递时,没有创建多余的对象,因此效率更高。
事实上,引用的底层必然是通过指针实现的,不过相较于指针,用户可以采用更简洁方便的写法。因此,引用是个好语法!
返回对象的值也会调用一次拷贝构造函数,而返回对象的引用并不会,可以自己试一下,在此不再赘述。
C++对象之谜(封装篇)的更多相关文章
- c++学习笔记之封装篇(上)
title: c++学习笔记之封装篇(上) date: 2017-03-12 18:59:01 tags: [c++,c,封装,类] categories: [学习,程序员,c/c++] --- 一. ...
- vue项目搭建 (二) axios 封装篇
vue项目搭建 (二) axios 封装篇 项目布局 vue-cli构建初始项目后,在src中进行增删修改 // 此处是模仿github上 bailicangdu 的 ├── src | ├── ap ...
- C++远征之封装篇(下)-学习笔记
C++远征之封装篇(下) c++封装概述 下半篇依然围绕类 & 对象进行展开 将原本学过的简单元素融合成复杂的新知识点. 对象 + 数据成员 = 对象成员(对象作为数据成员) 对象 + 数组 ...
- 6-C++远征之封装篇[上]-学习笔记
C++远征之封装篇(上) 课程简介 类(抽象概念),对象(真实具体) 配角: 数据成员和成员函数(构成了精彩而完整的类) 构造函数 & 析构函数(描述了对象的生生死死) 对象复制和对象赋值 ( ...
- v77.01 鸿蒙内核源码分析(消息封装篇) | 剖析LiteIpc(上)进程通讯内容 | 新的一年祝大家生龙活虎 虎虎生威
百篇博客分析|本篇为:(消息封装篇) | 剖析LiteIpc进程通讯内容 进程通讯相关篇为: v26.08 鸿蒙内核源码分析(自旋锁) | 当立贞节牌坊的好同志 v27.05 鸿蒙内核源码分析(互斥锁 ...
- Windows 7 封装篇(一)【母盘定制】[手动制作]定制合适的系统母盘
Windows 7 封装篇(一)[母盘定制][手动制作]定制合适的系统母盘 http://www.win10u.com/article/html/10.html Windows 7 封装篇(一)[母盘 ...
- Java-Runoob-面向对象:Java 封装
ylbtech-Java-Runoob-面向对象:Java 封装 1.返回顶部 1. Java 封装 在面向对象程式设计方法中,封装(英语:Encapsulation)是指一种将抽象性函式接口的实现细 ...
- 对象的可见性 - volatile篇
作者:汤圆 个人博客:javalover.cc 前言 官人们好啊,我是汤圆,今天给大家带来的是<对象的可见性 - volatile篇>,希望有所帮助,谢谢 文章如果有误,希望大家可以指出, ...
- 用类方法------>快速创建一个autorelease的对象,在封装的类方法内部
在封装的类方法内部,也就是+ (id)personWithName:(NSString *)name andAge:(int)age内部: 创建了一个person对象,并且创建了一个person*类型 ...
- 对象(类)的封装方式(面向对象的js基本知识)
上一个月一直忙于项目,没写过笔记,今天稍微空下来了一点 前几天在写项目的时候关于怎么去封装每一个组件的时候思考到几种方式,这里总结一下: 1.构造函数方式(类似java写类的方式):把所有的属性和方法 ...
随机推荐
- Spring Boot 自动配置注解源码解析
前言 为什么Spring Boot这么火?因为便捷,开箱即用,但是你思考过为什么会这么便捷吗?传统的SSM架构配置文件至少要写半天,而使用Spring Boot之后只需要引入一个starter之后就能 ...
- 绿色数字园区运维:一屏群集 3D 可视化智慧楼宇
前言 在"新基建"驱动的数字经济热潮下,智慧园区建设发展成为实现园区管理绿色化.现代化.智慧化的核心抓手.通过利用云计算.物联网.大数据等新一代技术手段,充分聚合园区内各类资源,全 ...
- eyebeam高级设置
概述 VOIP测试过程中,经常会用到各种各样的SIP终端,eyebeam是其中最常见的一种. 在eyebeam的配置option中,只有少量的配置选项,有些特殊的设置无法配置. 比如DTMF码的发码形 ...
- zzuli1895: 985的0-1串难题
//解法:用二分查找,如果当前位置是'1',则查找比这个位置多k+1个零的位置,如果当前位置是'0',则查找比当前位置多k个零的位置, 注意要在末尾添个最大的值 #include<iostrea ...
- 大数相加 a+b
#include<stdio.h> #include<string.h> #include<stdlib.h> #include<ctype.h> #i ...
- [转帖]oracle导出千万级数据为csv格式
当数据量小时(20万行内),plsqldev.sqlplus的spool都能比较方便进行csv导出,但是当数据量到百万千万级,这两个方法非常慢而且可能中途客户端就崩溃,需要使用其他方法. 一. sql ...
- SQLServer的varchar与nvarchar的学习之二
SQLServer的varchar与nvarchar的学习之二 背景 昨天简单总结了多种数据库 varchar和nvarchar的区别与关系 今天想着能够分析一下数据库文件. 计划使用winhex 查 ...
- [转帖]一文说清 Linux System Load
https://zhuanlan.zhihu.com/p/447661302 双十一压测过程中,常见的问题之一就是load 飙高,通常这个时候业务上都有受影响,比如服务rt飙高,比如机器无法登录,比如 ...
- [转帖]Influxdb 2.x 快速入门
Influxdb 2.x 快速入门 https://www.jianshu.com/p/268fca65f10e Influxdb是由Golang 构建的时序数据库,由于由Go语言构建使得其跨平台部署 ...
- [转帖]金仓数据库KingbaseES V8R6索引坏块故障处理
案例说明: 在执行表数据查询时,出现下图所示错误,索引故障导致表无法访问,后重建索引问题解决.本案例复现了此类故障解决过程. 适用版本: KingbaseES V8R3/R6 一.创建测试环境 # 表 ...