0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

以良好的方式编写C++ class

Linux爱好者 来源:博客园 作者:melonstreet 2022-06-29 14:07 次阅读

以良好的方式编写C++ class

假设现在我们要实现一个复数类complex,在类的实现过程中探索良好的编程习惯。

① Header(头文件)中的防卫式声明

complex.h:
#ifndef__COMPLEX__
#define__COMPLEX__
classcomplex
{

}
#endif

防止头文件的内容被多次包含。

② 把数据放在private声明下,提供接口访问数据

#ifndef__COMPLEX__
#define__COMPLEX__
classcomplex
{
public:
doublereal()const{returnre;}
doubleimag()const{returnim;}
private:
doubelre,im;
}
#endif

③ 不会改变类属性(数据成员)的成员函数,全部加上const声明

例如上面的成员函数:

doublereal()`const`{returnre;}
doubleimag()`const`{returnim;}

既然函数不会改变对象,那么就如实说明,编译器能帮你确保函数的const属性,阅读代码的人也明确你的意图。

而且,const对象才可以调用这些函数——const对象不能够调用非const成员函数。

④ 使用构造函数初始值列表

classcomplex
{
public:
complex(doubler=0,doublei=0)
:re(r),im(i){}
private:
doubelre,im;
}

在初始值列表中,才是初始化。在构造函数体内的,叫做赋值。

⑤如果可以,参数尽量使用reference to const

为complex 类添加一个+=操作符:

classcomplex
{
public:
complex&operator+=(constcomplex&)
}

使用引用避免类对象构造与析构的开销,使用const确保参数不会被改变。内置类型的值传递与引用传递效率没有多大差别,甚至值传递效率会更高。

例如,传递char类型时,值传递只需传递一个字节;引用实际上是指针实现,需要四个字节(32位机)的传递开销。但是为了一致,不妨统一使用引用。

⑥ 如果可以,函数返回值也尽量使用引用

以引用方式返回函数局部变量会引发程序未定义行为,离开函数作用域局部变量被销毁,引用该变量没有意义。但是我要说的是,如果可以,函数应该返回引用。

当然,要放回的变量要有一定限制:该变量的在进入函数前,已经被分配了内存。以此条件来考量,很容易决定是否要放回引用。而在函数被调用时才创建出来的对象,一定不能返回引用。

说回operator +=,其返回值就是引用,原因在于,执行a+=b时,a已经在内存上存在了。

operator + ,其返回值不能是引用,因为a+b的值,在调用operator +的时候才产生。

下面是operator+= 与’operator +’ 的实现:

inlinecomplex&complex::operator+=(constcomplex&r)
{
this->re+=r->re;
this->im+=r->im;
return*this;
}
inlinecomplexoperator+(constcomplex&x,constcomplex&y)
{
returncomplex(real(x)+real(y),//新创建的对象,不能返回引用
imag(x)+imag(y));
}

operator +=中返回引用还是必要的,这样可以使用连续的操作:

c3+=c2+=c1;

⑦ 如果重载了操作符,就考虑是否需要多个重载

就我们的复数类来说,+可以有多种使用方式:

complexc1(2,1);
complexc2;
c2=c1+c2;
c2=c1+5;
c2=7+c1;

为了应付怎么多种加法,+需要有如下三种重载:

inlinecomplexoperator+(constcomplex&x,constcomplex&y)
{
returncomplex(real(x)+real(y),
imag(x+imag(y););
}
inlinecomplexoperator+(constcomplex&x,doubley)
{
returncomplex(real(x)+y,imag(x));

inlinecomplexoperator+(doublex,constcomplex&y)
{
returncomplex(x+real(y),imag(y));
}

⑧ 提供给外界使用的接口,放在类声明的最前面

这是某次面试中,面试官大哥告诉我的。想想确实是有道理,类的用户用起来也舒服,一眼就能看见接口。

Class with pointer member(s):记得写Big Three

C++的类可以分为带指针数据成员与不带指针数据成员两类,complex就属于不带指针成员的类。而这里要说的字符串类String,一般的实现会带有一个char *指针。带指针数据成员的类,需要自己实现class三大件:拷贝构造函数、拷贝赋值函数、析构函数。

classString
{
public:
String(constchar*cstr=0);
String(constString&str);
String&operator=(constString&str);
~String();
char*get_c_str()const{returnm_data};
private:
char*m_data;
}

如果没有写拷贝构造函数、赋值构造函数、析构函数,编译器默认会给我们写一套。然而带指针的类不能依赖编译器的默认实现——这涉及到资源的释放、深拷贝与浅拷贝的问题。在实现String类的过程中我们来阐述这些问题。

①析构函数释放动态分配的内存资源

如果class里有指针,多半是需要进行内存动态分配(例如String),析构函数必须负责在对象生命结束时释放掉动态申请来的内存,否则就造成了内存泄露。

局部对象在离开函数作用域时,对象析构函数被自动调用,而使用new动态分配的对象,也需要显式的使用delete来删除对象。而delete实际上会调用对象的析构函数,我们必须在析构函数中完成释放指针m_data所申请的内存。下面是一个构造函数,体现了m_data的动态内存申请:

/*String的构造函数*/
inline
String::String(constchar*cstr=0)
{
if(cstr)
{
m_data=newchar[strlen(cstr)+1];//这里,m_data申请了内存
strcpy(m_data,cstr);
}
else
{
m_data=newchar[1];
*m_data='';
}
}

这个构造函数以C风格字符串为参数,当执行

String*p=newString("hello");

m_data向系统申请了一块内存存放字符串hello

e4d49818-f75f-11ec-ba43-dac502259ad0.png

析构函数必须负责把这段动态申请来的内存释放掉:

inline
String::~String()
{
delete[]m_data;
}

②赋值构造函数与复制构造函数负责进行深拷贝

来看看如果使用编译器为String默认生成的拷贝构造函数与赋值操作符会发生什么事情。默认的复制构造函数或赋值操作符所做的事情是对类的内存进行按位的拷贝,也称为浅拷贝,它们只是把对象内存上的每一个bit复制到另一个对象上去,在String中就只是复制了指针,而不复制指针所指内容。现在有两个String对象:

Stringa("Hello");
Stringb("World");

a、b在内存上如图所示:

e4f4a0d6-f75f-11ec-ba43-dac502259ad0.png

如果此时执行

b=a;

浅拷贝体现为:

e5013936-f75f-11ec-ba43-dac502259ad0.png

存储World的内存块没有指针所指向,已经成了一块无法利用内存,从而发生了内存泄露。不止如此,如果此时对象a被删除,使用我们上面所写的析构函数,存储Hello的内存块就被释放调用,此时b.m_data成了一个野指针。

来看看我们自己实现的构造函数是如何解决这个问题的,它复制的是指针所指的内存内容,这称为深拷贝

/*拷贝赋值函数*/
inlineString&String::operator=(constString&str)
{
if(this==&str)//①
return*this;
delete[]m_data;//②
m_data=newchar[strlen(str.m_data)+1];//③
strcpy(m_data,str.m_data);//④
return*this
}

这是拷贝赋值函数的经典实现,要点在于:

① 处理自我赋值,如果不存在自我赋值问题,继续下列步骤:② 释放自身已经申请的内存③ 申请一块大小与目标字符串一样大的内存④ 进行字符串的拷贝

对于a = b,②③④过程如下:

e50d80c4-f75f-11ec-ba43-dac502259ad0.pnge5193e50-f75f-11ec-ba43-dac502259ad0.pnge5230dae-f75f-11ec-ba43-dac502259ad0.png

同样的,复制构造函数也是一个深拷贝的过程:

inlineString::String(constString&str)
{
m_data=newchar[strlen(str)+1];
strcpy(m_data,str.m_data);
}

另外,一定要在operator = 中检查是否self assignment 假设这时候确实执行了对象的自我赋值,左右pointers指向同一个内存块,前面的步骤②delete掉该内存块造成下面的结果。当企图对rhs的内存进行访问是,结果是未定义的。

e52c144e-f75f-11ec-ba43-dac502259ad0.png

static与类

① 不和对象直接相关的数据,声明为static

想象有一个银行账户的类,每个人都可以开银行账户。存在银行利率这个成员变量,它不应该属于对象,而应该属于银行这个类,由所有的用户来共享。

static修饰成员变量时,该成员变量放在程序的全局区中,整个程序运行过程中只有该成员变量的一份副本。而普通的成员变量存在每个对象的内存中,若把银行利率放在每个对象中,是浪费了内存。

② static成员函数没有this指针

static成员函数与普通函数一样,都是只有一份函数的副本,存储在进程的代码段上。不一样的是,static成员函数没有this指针,所以它不能够调用普通的成员变量,只能调用static成员变量。普通成员函数的调用需要通过对象来调用,编译器会把对象取地址,作为this指针的实参传递给成员函数:

obj.func()--->Class::fun(&obj);

而static成员函数即可以通过对象来调用,也可以通过类名称来调用。

③在类的外部定义static成员变量

另一个问题是static成员变量的定义。static成员变量必须在类外部进行定义:

classA
{
private:
staticinta;//①
}
intA::a=10;//②

注意①是声明,②才是定义,定义为变量分配了内存。

④static与类的一些小应用

这些可以用来应付一下面试,在实现单例模式的时候,static成员函数与static成员变量得到了使用,下面是一种称为”饿汉式“的单例模式的实现:

classA
{
public:
staticA&getInstance();
setup(){...};
private:
A();
A(constA&rhs);
staticAa;
}

这里把class A的构造函数都设置为私有,不允许用户代码创建对象。要获取对象实例需要通过接口getInstance。”饿汉式“缺点在于无论有没有代码需要aa都被创建出来。下面是改进的单例模式,称为”懒汉式“:

classA
{
public:
staticA&getInstance();
setup(){....};
private:
A();
A(constA&rsh);
...
};
A&A::getInstance()
{
staticAa;
returna;
}

“懒汉式”只有在真正需要a时,调用getInstance才创建出唯一实例。这可以看成一个具有拖延症的单例模式,不到最后关头不干活。很多设计都体现了这种拖延的思想,比如string的写时复制,真正需要的时候才分配内存给string对象管理的字符串。

原文标题:漫谈 C++:良好的编程习惯与编程要点

文章出处:【微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

审核编辑:汤梓红
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 函数
    +关注

    关注

    3

    文章

    3865

    浏览量

    61307
  • C++
    C++
    +关注

    关注

    21

    文章

    2066

    浏览量

    72899
  • Class
    +关注

    关注

    0

    文章

    52

    浏览量

    19513
  • CONST
    +关注

    关注

    0

    文章

    43

    浏览量

    7995

原文标题:漫谈 C++:良好的编程习惯与编程要点

文章出处:【微信号:LinuxHub,微信公众号:Linux爱好者】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    C语言实现面向对象的方式 C++中的class的运行原理

    这里主要介绍下在C语言中是如何实现的面向对象。知道了C语言实现面向对象的方式,再联想下,C++中的class的运行原理是什么?
    发表于 10-21 09:00 834次阅读

    介绍MATLAB与C++的几种接口方式

    matlab调用c++,应该用mex把cpp编译成 .mex文件供MATLAB在命令行方式下调用吧看下面这片文章文章: 摘自北京理工大学BBSMATLAB是什么东东?不用我多说了,大批的高手会告诉你
    发表于 11-18 22:45

    学习学好c++的50条忠告

    却发现自己用的方法很拙劣时,请不要马上停手;请尽快将余下的部分粗略的完成保证这个设计的完整性,然后分析自己的错误并重新设计和编写(参见43); 43.别心急,设计C++class
    发表于 11-22 09:36

    如何为嵌入式应用编写优秀的C++程序代码

    是继承。但这样做的缺点是根据这种方式产生之类别的实例化对象可能需要一定的开销。‧编写‘聪明的’程序代码。开发人员可以用C++写出非常聪明简洁的程序代码。但C++也能让人写出相当晦涩难懂
    发表于 09-22 16:29

    CCS5.5编译一段C++程序,编译器不认识class认为没有定义

    在CCS5.5中编译一段C++程序,编译器不认识class这个关键字,认为没有定义?这是怎么回事呢?
    发表于 01-16 11:38

    请问我能在C++编写任何代码吗?

    当我听到PSoC 4和板与ARDUIO SHILDS兼容时,我想知道如何将AdUINO库的C++文件移植到PSoC Creator。我能在C++编写任何代码吗?有可能吗?如果我要编译PSoC
    发表于 06-11 09:05

    学习c++的经验分享!

    C++语言本身为主;42.当你写C++程序写到一半却发现自己用的方法很拙劣时,请不要马上停手;请尽快将余下的部分粗略的完成保证这个设计的完整性,然后分析自己的错误并重新设计和编写(参
    发表于 10-08 03:46

    如何用STM32CubeMX生成底层代码?代码中C++编写要注意哪些事项?

    如何用STM32CubeMX生成底层代码?单片机代码如何进行IDE的C++配置?代码中C++编写要注意哪些事项?C++实现时候遇到的情况有哪些?
    发表于 07-01 06:22

    如何用C++编写流水灯程序?

    为什么很少用C++开发单片机?如何用C++编写流水灯程序?
    发表于 09-30 08:27

    怎样使用C++编写Cortex-M系列MCU的程序呢

    C++是什么?C++的特点有哪些呢?怎样使用C++编写Cortex-M系列MCU的程序呢?
    发表于 12-23 06:31

    怎样用c++编写程序呢

    由于我们使用的是 ARM 的工具链 是gcc的,所以,我们大可以用c++编写程序,无论是 c++99 或c++11 还是 c++14,都是
    发表于 01-26 06:58

    如何编写cc++代码混编工程Makefile文件?

    如何编写cc++代码混编工程Makefile文件?
    发表于 03-09 06:55

    C++ Class library and Automati

    This file has two distinct features: A complete C++ class library (source code) that gives access
    发表于 08-08 21:48 9次下载

    异常安全的C++代码编写

    关于C++中异常的争论何其多也,但往往是一些不合事实的误解。异常曾经是一个难以用好的语言特性,幸运的是,随着C++社区经验的积累,今天我们已经有足够的知识轻松编写
    发表于 09-16 11:50 5次下载

    C++中struct和class的区别?

    C++中struct和class的区别是什么?C++中struct和class的最大区别在于:         struct的成员默认是公有的, 而
    的头像 发表于 03-10 17:41 599次阅读