关于C/C++的一些思考(3)
操作符重载函数(Operator Overload Function)的基本概念:
目的是以与对待内置数据类型相同的方式对待用户自定义类型(程序执行速度会受到影响),限制是不能随意选择函数名和参数个数(必须与重载的基本类型运算符保持一致);
编译器首先检查的表达式中的左操作数是否为对象类型,若是则在其类定义中搜寻对应的operator开头的方法,最后进行函数名字替换,参数类型检查等等操作(作为重载操作符函数的参数,一般使用const的索引类型,除非某个参数需要改变);
如果左操作数为数值类型,则检测右操作数是否为类类型,若是,则在友元函数中寻找对应的重载操作符函数,如果没有相关定义则为错误;如果左右操作数都不是程序员定义类型,则按照基本类型运算符进行操作;
重载操作符函数解决的只是一个可读性问题,也就是让对象类型之间的操作看起来也像内置类型,仅此而已,编译器会将这种类型的操作替换回函数调用的形式
z=x+y; ->>> z=operator+(x,y);或者z=x.operator+(x,y)
赋值操作符重载函数需要注意三个地方:
为了不发生内存共享问题,需要重新创建动态内存,并复制传入对象的动态内存;
为了不发生自我赋值时的内存删除问题,需要检查是否传入对象为当前对象本身;
为了实现链式表达式,需要将返回值设置为引用对象类型
String& String::operator=(const String& s) {
if(&s == this) return this;
delete str;
len=s.len;
str=new char[len+];
if(str==NULL) exit();
strcpy(str, s.str);
return this;
}
重载操作符函数定义为类成员函数:
由于编译器会自动添加指向当前对象的this指针,所以定义他们的时候代表左操作数的参数可以缺省;
如果我们在类成员函数和全局函数中都定义了针对同一个类型的重载操作符函数,当使用操作符表达式时会产生二义性,编译错误;但使用显示的函数调用则不会有错;较好的解决办法是将重载操作符函数定义为类型的友元函数:
class Complex {……Complex& operator+(const Complex &b) const {……}};
Complex& operator+(const Complex &a, const Complex &b) {……}
Complex a,b,c,d;
c=a+b;//二义性错误
c=a.operator+(b); //正确
c=operator+(a,b); //正确
d=(a.operator+(b)).operator+(c); //正确- 如果作为类成员函数出现的重载操作符函数,如果不会修改当前对象的任何类成员变量,则可将函数声明为const(函数的声明和定义两个地方都需要添加);如果某个参数不会在函数内部改变,则良好的设计为加上const参数;
- 为了支持混合类型运算的隐式转换,转换构造函数为一种解决办法,重载操作符函数为另一种解决办法;
Rational r; r+=;
Rational(int x);//将4转换为临时匿名Rational对象,然后调用重载操作符函数
void operator+=(Rational &r, Rational &x);
void Rational::operator+=(int x);//r.operator+=(4);
void operator+=(Rational &r, int x);//::operator+=(r,4); 不要对实现了重载运算符函数的数值类的转换构造函数使用explicit;否则在函数调用中试图使用基本类型参数替代该类型的对象的操作就会出错(只能显示转换)
为了使得某个类对象既可以与类对象进行操作,也可以与基本类型进行操作,可以进行如下设计:
explicit Rational(int x);//转换构造函数
void Rational::operator+=(Rational &r);
Rational a,b; a+=b; a+= Rational();这样的设计就可以使得r既可以是类对象类型,也可以是内置数值类型;
作为类成员的重载操作符函数只允许左边的操作数为对象类型,为了使得内置数值类型也可以为左边的操作数,可以进行如下设计:class Rational {
friend Rational& operator+=(const Rational &x, const Rational &y);
friend Rational& operator+=(const Rational &x, long y);
friend Rational& operator+=(long x, const Rational &y);
};
//全局友元函数
Rational& operator+=(const Rational &x, const Rational &y) {……}
Rational& operator+=(const Rational &x, const long y) {……}
Rational& operator+=(long x, const Rational &y) {……}友元函数除了可以访问目标类所有成员之外,与目标类没有任何关系;重载所有可能使用到的操作符函数就可以避免隐式调用类型转换构造函数,加快程序速度;
按值传递对象时容易发生的错误(共享同一份动态内存,多次调用析构函数):
隐式调用拷贝构造函数,其将使用实际参数对象初始化形式参数对象;或者赋值操作符重载函数,使用实际参数对象赋值形式参数对象;(所以如果将拷贝构造函数,或者赋值操作符重载函数显式定义为private函数则可禁止按值传递对象)
当对象内部有成员指针变量指向动态堆内存,如果该对象按值传递,则函数内部的形式参数对象会与实际参数对象共享同一份动态堆内存数据;
形式参数对象被销毁时会调用其析构函数将内部成员指针变量指向的动态堆内存销毁,而这正是与实际参数对象共享的那一份的动态堆内存;
当实际参数对象被销毁的时候会再次调用析构函数试图再次销毁已经被销毁的动态堆内存,此时会发生重复删除内存的错误;
动态管理内存的类设计时,需要考虑如下函数:
缺省构造函数:当提供其他构造函数后,系统不会主动提供缺省构造函数
Rational(int a=0, int b=1);
拷贝构造函数:系统提供的拷贝构造函数仅为浅赋值,遇到动态内存问题可能出问题
Rational(const Rational &r);
多个转换构造函数:可以使用多种其他类型初始化本类类型的对象
Rational(const char *r);
Rational(int c);
……
多个赋值运算符重载函数:使用多种其他类类型对本类类型对象进行赋值,并可以避免在调用赋值运算符重载函数时调用转换构造函数生成适合类型
Rational& operator=(const Rational& r);
Rational& operator=(int c);
……
析构函数:释放资源
~Rational();
数据通信方式(对于C++中不同程序段之间的通信而言,总是使用最低程度的耦合度:首先考虑使用函数内部的局部变量,然后考虑使用类成员变量,其次考虑使用函数形式参数,最后才考虑使用全局变量)
全局变量或者静态变量
方法参数
类成员变量
方法的局部变量
复合对象的创建和销毁:
创建对象时,其构造函数在所有成员变量创建(分配内存,初始化)后才进行调用;并且成员变量的创建顺序是按照其在类定义中声明的先后顺序进行;
如 果有继承关系,则基类优先于所有派生类进行创建(也就是在调用派生类的构造函数之前进行调用),如果没有在成员初始化列表显示指定调用基类的哪一个构造函 数,则缺省调用无参构造函数(所以无参构造函数还有这一功能),但是最好使用成员初始化列表显示调用,从而避免不必要的函数调用;
C++类定义中不能对基本和类变量进行显示初始化;数值变量保持未初始化,类变量使用无参缺省构造函数进行;而类构造函数是在执行完所有数据成员的构造函 数之后执行(所以作为成员变量的类对象需要有无参构造函数,否则会发生错误);唯一的例外是static const变量可以在类定义的时候初始化;
C++类成员对象有两种设置值的方式:一种是利用无参构造函数初始化,再通过其他方式赋值;另一种是利用成员初始化列表调用指定参数的构造函数初始化;前者效率低下,成员对象被多次设置,后者仅设置一次;
class Rational {
Point p1;Point p2;
Rational(const Point& pp1, const Point& pp2) {p1=pp1;p2=pp2}
};上述类定义中,p1和p2会首先调用其无参缺省构造函数进行初始化,然后调用赋值操作符重载函数进行赋值:
class Rational {
Point p1; Point p2;
Point p3&;
const int WEIGHT;
Rational(const Point& pp1, const Point& pp2, Point &pp3, int w):
p1(pp1),P2(pp2),p3(pp3),WEIGHT(w) {}
};上述类定义中,p1和p2直接调用其拷贝构造函数进行初始化(从而避免不必要的构造函数调用);成员初始化列表不会改变类成员变量的初始化顺序(由类定义顺序决定),一般不在构造函数声明的地方定义,而是在实现的地方定义;
如果某个类成员变量为const类型,则必须使用成员初始化列表,否则其不能再赋其他值;如果某个类成员变量为引用类型,则也必须使用成员初始化列表,否则其不能其赋其他值;
一个类型使用它自身作为类成员变量的时候,不能直接使用Rational r;因为类成员变量在类创建之前创建,但此时Rational并没有创建,所以语法错误;但可以使用Rational *ptrr;或者Rational &refr;实现对自身类型的引用;
如果将某个类成员变量声明为static,则即使他们声明为private成员,也可以在类定义之外进行初始化:(可使用类作用域运算符访问,也可使用目标对象访问)
int Rational::count=0; p1.count=9;有继承关系的派生类的实例在内存中不仅保存自身的所有非static成员变量,还保存所有基类的所有非static成员变量(包括private,protected和public)
销毁对象时,首先调用对象的析构函数,执行资源释放操作,然后以成员变量初始化创建相反的顺序进行销毁(也就是从类定义中最后一个成员变量开始进行销毁);如果有继承关系,在派生类销毁完毕之后再进行派生类的销毁;
成员初始化列表的目的是避免在复合类构造函数被调用之前,编译器调用成员对象类的缺省无参构造函数(如果没有的话会报错),而在构造函数中又会重新进行赋 值,所以在成员初始化列表中强制要求调用的构造函数类型。成员初始化列表防止语法错误并改善性能;函数声明中为参数提供的缺省值只能自右向左依次进行,调 用的时候实际参数赋值的顺序为自左向右依次进行;成员初始化列表使得具有继承关系的类创建更有效率;
调用一个派生类(Drived class)实例的某个函数时,实际调用可能为:
调用的函数来自基类,并在派生类中无定义;
调用的函数来自基类,并在派生类中重新定义(如果函数名和标签完全相同,则为over-write,如果仅函数名相同,标签不同,则为隐藏);
调用的函数来自派生类,基类无定义;
虚函数(涉及多态,指针),基类定义的函数由virtual关键字,并且派生类中有函数标示符完全相同的实现;
C++的继承方式:(不同的继承模式实质上是对基类中成员访问权限的一种修改,只能向可见范围变小的方向修改,也就是说即使使用private继承,也可 以通过public: Base::publB将基类的成员调整为原始访问权限,但是不能放大权限;这些修改影响子类,子类的客户类和子类的子类。虚拟函数仅在public继承 模式下才有效)
public:公共继承:客户在子类中对基类中成员的访问权限与基类保持一致,子类中可以访问父类中的public和protected成员;子类的客户类可以访问子类和基类中的public成员;完全的类扩展;
protected:受保护继承:从父类中继承的public和protected成员在子类中变成protected,也就是说子类的客户仅能访问子类 的public成员,子类对象可访问父类中的public和protected成员,常用于基类的一些方法仅提供派生类访问;
private:私有继承:从父类中集成的public和protected成员在子类中变成private,也就是说即使是子类,子类的子类也不能再访问这些父类的成员变量,常用于派生类完全重新定义基类的方法;
派生类中非虚拟函数的查找过程以及函数名隐藏问题:
从当前类定义的类成员函数表开始向基类方向进行搜索,一旦找到对应函数头的函数定义则结束搜索过程;
这个过程中一旦编译器找到函数名相同的函数声明就不再搜索,如果参数列表不匹配则报错而不会继续搜索(即使基类中有其他相同函数名的函数),这就是函数名 覆盖;(函数名相同的设计方法,对于同一作用域是函数名重载,对于不同作用域是隐藏,也就是同一个类定义与父子类定义中);
如果直到基类都没有找到对应的函数名,则搜索全局函数,以及来自其他文件的extern函数的范围;否则调用失败:
函数重载仅仅适用于同一类定义中,相同函数名有不同的参数列表,这时候编译器会根据参数列表选择正确的函数。但是在继承关系中,基类和派生类中也可能出现 这种函数名相同但是参数列表不同的情况,但是这个时候编译器并不会使用重载函数的策略,而是自底向上地搜索函数名相同的函数,一旦找到之后就不再寻找,此 时如果参数列表不匹配则会报错。这是一种函数名覆盖的机制。这应该和继承关系中的函数重写区别(函数名,参数列表,返回值完全相同),也应该和虚拟函数区 别(使用virtual修饰的重写函数);解决这个问题的设计如下:
class Account {void deposit(double amount) {……}}
class CheckingAccount: public Account {
void deposit(double amount) {Account::deposit(amount) {……}
void deposit(double amount, double free) {……}
}; CheckingAccount ca;当ca实例试图调用Account的deposit(double)方法时,如果CheckingAccount没有定义 deposit(double),则客户只能调用deposit(double, double),父类的deposit(double)被隐藏;因此上述设计在CheckingAccount中定义一个 deposit(double),然后再调用基类的方法,注意一定需要使用作用域运算符Account::deposit(double),否则调用自 身;
关于C/C++的一些思考(3)的更多相关文章
- 领域驱动和MVVM应用于UWP开发的一些思考
领域驱动和MVVM应用于UWP开发的一些思考 0x00 起因 有段时间没写博客了,其实最近本来是根据梳理的MSDN上的资料(UWP开发目录整理)有条不紊的进行UWP学习的.学习中有了心得体会或遇到了问 ...
- 关于面试题 Array.indexof() 方法的实现及思考
这是我在面试大公司时碰到的一个笔试题,当时自己云里雾里的胡写了一番,回头也曾思考过,最终没实现也就不了了之了. 昨天看到有网友说面试中也碰到过这个问题,我就重新思考了这个问题的实现方法. 对于想进大公 ...
- 关于 CSS 反射倒影的研究思考
原文地址:https://css-tricks.com/state-css-reflections 译者:nzbin 友情提示:由于演示 demo 的兼容性,推荐火狐浏览.该文章篇幅较长,内容庞杂,有 ...
- 关于.NET参数传递方式的思考
年关将近,整个人已经没有了工作和写作的激情,估计这个时候很多人跟我差不多,该相亲的相亲,该聚会喝酒的聚会喝酒,总之就是没有了干活的心思(我有很多想法,但就是叫不动我的手脚,所以我只能看着别人在做我想做 ...
- 使用NUnit为游戏项目编写高质量单元测试的思考
0x00 单元测试Pro & Con 最近尝试在我参与的游戏项目中引入TDD(测试驱动开发)的开发模式,因此单元测试便变得十分必要.这篇博客就来聊一聊这段时间的感悟和想法.由于游戏开发和传统软 ...
- OpenGL shader 中关于顶点坐标值的思考
今天工作中需要做一个事情: 在shader内部做一些空间距离上的计算,而且需要对所有的点进行计算,符合条件的显示,不符合条件的点不显示. 思路很简单,在vertex shader内知道顶点坐标,进行计 ...
- 关于领域驱动设计(DDD)中聚合设计的一些思考
关于DDD的理论知识总结,可参考这篇文章. DDD社区官网上一篇关于聚合设计的几个原则的简单讨论: 文章地址:http://dddcommunity.org/library/vernon_2011/, ...
- 关于bug分析与异常处理的一些思考
前言:工作三年了,工作内容主要是嵌入式软件开发和维护,用的语言是C,毕业后先在一家工业自动化控制公司工作两年半,目前在一家医疗仪器公司担任嵌入式软件开发工作.软件开发中,难免不产生bug:产品交付客户 ...
- 【数据库】_由2000W多条开房数据引发的思考、实践----给在校生的一个真实【练耙场】,同学们,来开始一次伟大的尝试吧。
× 缘起---闲逛博客园 前几天的时候,在某一QQ群看到一条消息“XXX酒店开房XXXBTXX迅雷BT下载”,当时是一目十行的心态浏览,目光掠过时, 第一反应我想多了~以为是XX种子(你懂的~ ...
- 对于多个数据库表对应一个Model问题的思考
最近做项目遇到一个场景,就是客户要求为其下属的每一个分支机构建一个表存储相关数据,而这些表的结构都是一样的,只是分属于不同的机构.这个问题抽象一下就是多个数据库表对应一个Model(或者叫实体类).有 ...
随机推荐
- VS2010 AnkhSvn
有时候打开别的项目用的是 TFS等 造成 VS本来用的AnkhSvn失效了 .工具---选项----插件选择---
- Luogu P1638 逛画展 【二分答案】
题目描述 博览馆正在展出由世上最佳的 M 位画家所画的图画. wangjy想到博览馆去看这几位大师的作品. 可是,那里的博览馆有一个很奇怪的规定,就是在购买门票时必须说明两个数字, a和b,代表他要看 ...
- 实现自己的ArrayList
最近在学习数据结构和算法,书上有个ArrayList的简单实现,写的很不错. package cn.sp.test4; import java.util.Iterator; import java.u ...
- LightOj 1236 Pairs Forming LCM (素数筛选&&唯一分解定理)
题目大意: 有一个数n,满足lcm(i,j)==n并且i<=j时,(i,j)有多少种情况? 解题思路: n可以表示为:n=p1^x1*p2^x1.....pk^xk. 假设lcm(a,b) == ...
- -Dmaven.multiModuleProjectDirectory system property is not set. Check $M2_HOME environment variable and mvn script match问题处理
使用MyEclipse运行Maven项目时报如下错误信息: -Dmaven.multiModuleProjectDirectory system property is not set. Check ...
- NSSet转成NSArray 以及NSSortDescriptor的使用
//如果想排序以后再取,可以这样:NSSet *users = [groupUser users]; //如果是存的字典,则key后面写的是想按照哪个关键字进行排序 NSSortDescriptor ...
- Web自动化测试框架-PO模式
Web自动化测试框架(WebTestFramework)是基于Selenium框架且采用PageObject设计模式进行二次开发形成的框架. 一.适用范围:传统Web功能自动化测试.H5功能自动化测试 ...
- CF932C Permutation Cycle
思路: 构造. 实现: #include <bits/stdc++.h> using namespace std; ]; int main() { int n, a, b; while ( ...
- poj2718 Smallest Difference
思路: 暴力乱搞. 实现: #include <iostream> #include <cstdio> #include <sstream> #include &l ...
- 转载:如何使用RFT自动打开IE
如何在RFT测试脚本中打开IE浏览器? 第一步,配置应用程序进行测试: “配置”菜单 ——> “配置应用程序进行测试...”,进入下面这个界面,默认三个自带的应用程序,点击“添加”加入IE. ...