关于C/C++的一些思考(1)
阅读原文时间:2023年07月08日阅读:1

C++的前世今生:

  • C的结构化思想;
  • Ada的模版思想;
  • Fortran的运算符重载思想;
  • Simula的OO思想:封装,继承,多态;

C++类型描述了变量的三个特征:

  • 该类型在内存中占用物理空间的大小(空间读取范围);
  • 该类型的值的合法的取值范围(位模式解释方法);
  • 合法的操作集(数据的用法);

C++的整数类型有两类修饰符(Qualifier):

  • signed和unsigned:调整整型数值的取值范围(不改变内存大小,仅改变数值读取模式,unsigned表示读取模式为非负数);整数类型 (int)缺省为signed int(signed和unsigned可以与short和long一起使用,修饰符顺序没有关系);

  • short和long:调整整型数值的物理空间大小(改变内存大小,不改变数值读取模式,目的是节省空间耗用或者增大取值范围);一般平台下的short int总规定是16位,long int总是32位,int由具体实现决定,所以移植性导向的软件(嵌入式或者通信系统)较少用int类型(他们之间的关系为short<=int& lt;=long);

++和—作为自增和自减操作:

  • 他们实现了汇编语言类型的处理,以不可中断的高优先级进行加1或者减1,此种方式产生的目标代码具有更高的效率:(浮点数也可以使用这两种操作);

  • 后缀表达所得到的返回值是一个数值(这一性质决定了其不可作为=的左值),并且仅有他们可以在不显式使用=的情况中改变操作数本身的值;

  • 前缀表达式得到的返回值的是一个变量,所以可以获取地址,作为左值。对于表达式num=5;total=num+ (++num);而言,最终值是12,表示优先处理+的两个加数,然后仅当进行+操作的时候才把num的值从内存取出(之前都是符号表示);

二者的区别是:

  • 前缀式先将操作数增1(或减1),然后将变量本身返回并参与表达式的运算。后缀是先返回变量的原始值参与表达式的运算,然后再在变量本身做加1(或减1)运算;I++返回的是值,不能用于左值,++I返回的是I变量本身,所以可以作为左值;

  • lvalue 指那些单一表达式结束之后依然存在的持久对象。例如: obj,*ptr, prt[index], ++x 都是lvalue。rvalue 是指那些表达式结束时(在分号处)就不复存在的临时对象。例如:1729 , x + y , std::string("meow") , 和 x++ 都是 rvalue;

  • int i=0; cout<<i<<" "<<i++<<endl;上述语句中,每一个<<符号表示一次独立的函数调用,如果存在多个函数调用,那么参数值 会从右到左处理一遍,之后再从左到右调用每个函数。从右到左处理参数是为了满足 C/C++ 的变参数函数的要求,而从左到右的调用函数则是与书写习惯相符合的,所以打印的结果为1,0。那么,这个语句中,先把 i 的值赋给第二个输出流操作符,然后进行自加,再将 i 的值(自加之后的)赋给第一个输出流操作符;printf("%d ,%d ", i , i++);由于是一次函数调用,所以打印结果都为0;

对于C++中用户定义类类型,使用四种方法可以将一种类型转换成另一种类型:

  • class B : public A { ……}

    B公有继承自A,并且可是间接继承;这样B类对象可以转化成A类对象;

  • class B { operator A( ); }

    B实现了隐式转化为A的转化,转换函数,不能有返回值和参数,并且为非静态,非有元函数;这样当遇到语句A=B的时候,B就调用poerator A()方法转换成A类型;

  • class A { A( const B& ); }

    A实现了non-explicit的参数为B(可以有其他带默认值的参数)构造函数;这样当初始化A类对象的时候,A a=B;就可以调用此构造函数;

  • A& operator= ( const A& );

    赋值操作,虽不是正宗的隐式类型转换,但也可以勉强算一个;这样当遇到语句B=A的时候,B就调用重载赋值操作符转换成A;

两类特性决定C++中程序变量的特性:

  • c++中变量的六种词法作用域(Lexical Scope),为程序变量的正确使用和操作,以及方便阅读,词法作用域描述变量的编译阶段特性(满足程序文法要求);并且变量或者函数的名字在同一作用域中要求唯一性(嵌套除外):块,函数,文件,程序,类,名字空间;

  • C++中变量的三种存储类别(Storage Type):变量名和其在内存中对应单元的关联属于合法时(内存有效分配)的一段有效的运行时间段内,决定变量的物理存储空间。词法作用域描述变量的编译 阶段特性,存储类别描述变量的运行阶段特性:静态固定内存,程序栈内存,程序堆动态内存;

C++程序的变量定义中,三种可以确定存储访问类别及其他特性的关键字:

  • auto:块或函数作用域中定义为局部变量的自动变量(缺省),存储于程序栈内存;

    好处是最小化了变量的作用域(名字唯一性冲突),最小化了内存的时间占用率(内存空间利用率);坏处是重复进行内存分配、释放影响性能,并且其大小在编译阶段就需要确定;(如果使用动态内存的话,就相当于使用其管理的复杂性来换取数据定义的灵活性);

    编译器不会帮助进行初始化,仅当程序执行到某一个作用域时,该作用域中的auto变量才存在;

    函数的形式参数也是该函数作用域中的auto变量,利用在函数调用中的实际参数的值进行初始化;

  • extern:定义在函数或者块之外的变量,目的是为访问其他文件作用域的变量或函数,而定义的全局变量,存储于固定内存;

    全局变量(全局变量缺省为外部变量)在main函数之前进行内存分配,并进行初始化(若没有主动初始化,则系统缺省初始化为0),在main函数执行完全之后才释放;

    优点是一次进行内存计算和分配,提升程序执行速度;全程序空间和时间都可以使用,减少函数参数个数,减少栈空间耗用;坏处是内存占用率高,代码可读性差,可修改性差;

    为了访问另一个文件中的数组,使用extern double cout[];但是不能写明元素个数,因为此处仅是引用申明,不是定义;extern本身有两层意义:一是当前文件中定义的变量可以在其他文件被访问(缺 省extern也可以);二是当前文件中申明的变量是引用其他文件定义的变量;
    extern int count=0; 或者int count; 表示count为当前文件定义的变量;
    extern int count; 表示count为引用其他文件的一个变量(如果其他文件也没有定义,则程序连接阶段将出现缺少定义的error);

  • static:与作用域无关(全局函数或者全局变量除外),仅影响变量的存储域,存储于固定内存中;

    被static修饰的变量,在main函数执行之前分配内存,全局变量分配空间的时候进行初始化;可以为文件作用域的全局变量,也可以为块或者函数作用域中的局部变量;

    有三种上下文可以使用static修饰(设置作用域,设置存储域);

    修饰【全局变量,全局函数】的时候表明仅同文件内部可见,其他文件不能通过extern访问,并且不改变其存储位置和生命周期;另外一个好处是其他文件可以使用相同的名字定义变量和函数,也就是全局唯一性消失;

    修饰【语句块,函数内的变量】的时候表明此变量可重复使用,也就是独立于语句块或者函数的作用域存在,存储空间为固定内存,生命周期为全程序(main函 数调用之前就已经分配内存并初始化,初始化为'0'),但引用域仍为该语句块,也就是仅当程序执行到其所在作用域时,该变量才可见;

    修饰【类定义中的变量,函数】,表明此成员为类级别,独立于实例对象(对static函数而言,不再能访问非静态类域或者非静态函数);常用于针对某种类型的所有对象的信息簿记,或者工具类或者变量;

  • register:cache中的变量,高速缓存中;用于存储那些会多次被使用到的变量,一般由编译器自行决定哪些变量应该作为register类型,程序员只能给出建议;

函数调用中,若实际参数跟形式参数的类型不相同,或者赋值操作中,有三种处理方法:

  • 一种是如果可以通过类型提升和隐式类型转换将实参类型转换为形参类型(类型不同,但需要具有相同的操作集合,也就是相容类型),则程序可以正确编译,首先执行的是位宽提升,如果不能达到目的,才使用隐式类型转换:

    如果是较短的位宽转换为较长的位宽,无信息损失,则正确执行;

    如果是较长位宽的类型转换为较短位宽类型之后,则会造成类型精度的丢失,但程序还是继续执行;

  • 对于相容类型而言,程序员可以使用显示的类型转换操作'(int)x'进行转换操作;显示的转换操作可以使得程序可读性更强;指针类型不支持隐式的转换 (除非父子继承类型),可以通过显示的转换(int*)&x;但是如果x的类型不是int,则编译器会按照int位模式进行解释,即使x的物理位 模式是double;或者使用程序员自定义的opeerator重载函数;

  • 对于非相容类型而言,如果形参和实参的类型不能相互转换(如内置类型int和程序员定义类型Node,他们具有完全不同的操作集合,则不能实现转换),此时编译程序出现错误;当然除非有程序员自定义的类型转换函数,或者转换构造函数;

const修饰变量的意图:

  • 使用const修饰一般数值变量,则这个变量不能进行赋值操作,甚至不能作为指针和引用变量赋值操作的右值(除非指针或者引用变量也为常量),目的都是为了防止任何直接或者间接地改变(const int a与int const a相同);

  • 使用const修饰指针,引用类型(const int *a或者int const *a;引用的声明形式类似),如const int *ptrtemp; const int &reftemp;表示由他们指向的内存内容不能有任何直接或者间接地改变,除去索引之后和一般索引变量的限制相同;

  • 使用const修饰指针变量本身,如int * const ptrtemp;表示指针变量本身不能再指向其他地址,也就是只能在变量初始化时进行赋值,这一性质和索引变量相同,索引变量可以看做指针变量本身const修饰,同时指针变量指向的内存内容可以改变:

    const int val=10;

    int *p=&val;//错误

    int &r=val;//错误

    const int *constP=&val;//正确

    const int &constR=val;//正确

    Account* largerOne(const Account &a1, const Account &a2) {

    return (a1,value > a2.value) ? &a1:&a2;}//错误

    由于传入参数为const,如果试图将其作为返回值,则返回值类型需要设置为const

  • 同时const还可以传递程序员的意图:比如const的函数参数是作为输入值,非const的函数参数是作为修改值或者输出值,const的函数返回值 要求同样是const的右值变量,const的类成员函数表示const *this的缺省参数,也就是不能修改任何实例的类变量;

类成员函数的重载、覆盖和隐藏之间的区别:

  • 重载(同一个类的成员函数之间的动作,基本语法):

    1) 相同范围,同一个类
    2) 函数名字相同
    3) 参数(顺序,个数,类型)不同
    4) virtual等函数修饰符不能有效区分重载函数
    5) 重载函数不能有效区分:const和非const的参数类型;
    6) 重载函数不能有效区分:索引类型与一般类型的参数
    7) 函数返回值不能有效区分重载函数;

  • 覆盖(派生类和基类的成员函数之间的动作,多态机制):

    1) 不同范围,派生类和基类
    2) 函数名字相同
    3) 参数(顺序,个数,类型)完全相同
    4) 位于基类的函数必须有virtual关键字

  • 隐藏(派生类的函数屏蔽了与其同名的基类的函数,基本语法):

    1)不同范围,派生类和基类
    2) 函数名字相同
    3) 如果只是函数名字相同,参数不同,则无论是否有virtual关键字,派生类的函数都将屏蔽基类的函数
    4) 如果函数名字和参数都完全相同,并且基类函数没有virtual关键字(有的话就是覆盖),则派生类的函数都将屏蔽基类的函数

高质量程序设计的几个要点:

  • 将职责从客户端推向服务器端;

  • 限制客户端和服务器端的信息共享:

    数据封装(Encapsulation):客户通过标准接口访问数据;

    信息隐藏(Hide):客户不关心服务器的内部情况也可使用服务;

  • 避免将应该在一起的代码分开,在代码块间增加无谓的通信流(传入参数和返回值),增加模块之间的依赖性:

    低耦合性(Coupling)是模块间的关系,避免应该分开的代码合在一起;隐式耦合度表示多个函数使用全局变量的个数(涉及到初始化的问题,最强的耦合度),显式耦合度表示函数调用之间传入和输出的参数的个数;不适当的耦合度都是因为程序功能不恰当分配造成的;

    高内聚性(cohesive)是模块内部逻辑间的关系,避免不相关的操作合在一起;

  • 使用代码本身而不是注释阐述程序意图,反过来注释不应该只是解释程序本身的逻辑,而是阐述程序的意图和注意事项;

  • 模块设计的时候考虑可维护性(Maintainability)和可重用性(Reusability)