c++ 细节整理

命名空间

  • 引入了命名空间这个概念,专门用于解决上面的问题,它可作为附加信息来区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上,命名空间就是定义了一个范围。
namespace namespace_name {
   // 代码声明
}
  • 您可以使用 using namespace 指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称。这个指令会告诉编译器,后续的代码将使用指定的命名空间中的名称。
  • using 指令也可以用来指定命名空间中的特定项目。例如,如果您只打算使用 std 命名空间中的 cout 部分,您可以使用如下的语句:
using std::cout;

函数签名

  • 每个函数的签名式也就是函数的声明也就是返回值类型和参数类型
  • 一个函数的签名等同于函数的类型
    对于函数指针的声明
int (*add) (int,int);
//add就是一个函数签名为int (int,int)类型函数的指针

typedef 作用

  • 定义一种类型的别名,而不只是简单的宏替换。可以用作同时声明指针型的多个对象。比如:
typedef char* PCHAR; // 一般用大写
PCHAR pa, pb; // 可行,同时声明了两个指向字符变量的指针
  • 不是在预编译进行的是在编译的时候进行的
  • 用在旧的C的代码中(具体多旧没有查),帮助struct。以前的代码中,声明struct新对象时,必须要带上struct,即形式为: struct 结构名 对象名,经常多写一个struct太麻烦了,于是就发明了:
typedef struct tagPOINT  
{  
    int x;  
    int y;  
}POINT;  
POINT p1; // 这样就比原来的方式少写了一个struct,比较省事,尤其在大量使用的时候 
  • 用typedef来定义与平台无关的类型
//比如定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型为:

typedef long double REAL; 
//在不支持 long double 的平台二上,改为:

typedef double REAL; 
//在连 double 都不支持的平台三上,改为:

typedef float REAL;  

也就是说,当跨平台时,只要改下 typedef 本身就行,不用对其他源码做任何修改。标准库就广泛使用了这个技巧,比如size_t。另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健(虽然用宏有时也可以完成以上的用途)

  • 为复杂的声明定义一个新的简单的别名。方法是:在原来的==声明里==逐步用别名==替换==一部分复杂声明,如此循环,把带变量名的部分留到最后替换,得到的就是原声明的最简化版
  • 记住,typedef是定义了一种类型的新别名,不同于宏,它不是简单的字符串替换。
  • 而typedef是在编译时处理的。它在自己的作用域内给一个已经存在的类型一个别名,
    typedef 就像 auto,extern,mutable,static,和 register 一样,是一个存储类关键字。这并是说 typedef 会真正影响对象的存储特性;它只是说在语句构成上,typedef 声明看起来象 static,extern 等类型的变量声明。下面将带到第二个陷阱:
    typedef register int FAST_COUNTER; // 错误
    编译通不过。问题出在你不能在声明中有多个存储类关键字。因为符号 typedef 已经占据了存储类关键字的位置,在 typedef 声明中不能用 register(或任何其它存储类关键字)

explicit

  • 对于类的构造函数前加上explicit 可以阻止隐式类型转换但是可以被用来显示类型转换

编译依存关系

  • c++ 并没有把 将接口从实现中分离 做的很好
  • 在定义一个类的时候会包含一些成员变量,那么就需要include 这些成员变量的头文件才能使得这个类通过编译,如果这些成员的头文件中出现修改,或者这些头文件当中include的头文件出现修改那么所有使用当前定义的类的文件都会被重新编译,这样的编译依存关系会对许多项目造成难以形容的灾难。
  • 如果使用对象的引用或者对象的指针就可以完成任务,那就不要使用对象本身,因为引用和指针只需要一个类型的声明式就可以,而如果定义某个类型的对象,就需要用到该类型的定义。
  • 如果可以尽量使用class的声明式替换class的定义式。当声明一个函数的时候它用到某个class的时候并不需要该class的定义,纵使是以传值的方式传递该类型的参数的或者返回值也不需要定义。

- 将提供class定义式(通过#include完成)的义务从函数声明所在的头文件转移到内含函数调用的客户文件当中,便可以将非必要类型定义与客户端之间的编译依存性去除。


地址对齐

#include<stdio.h>
int main()
{
    int a;
    char b;
    int c;
    printf("a:%x  b:%x  c:%x\n",&a,&b,&c);  //输出: a:22ff1c  b:22ff1b  c:22ff14
    return 1;
}
  • 代码中为定义的变量a,b,c分配了内存单元,分配内存单元是从大地址开始分配的,可见a的内存地址大于b的地址。
  • 从代码中可以看出为三个连续定义的变量的内存分配是不连续的。这就是因为地址对齐的原因。
  • 地址对齐概念

    地址对齐其实就是CPU设计中的一个==时空权衡==,这里采用的是==空间换时间的==。主要的解决问题是如何能过快速的读取给定的一个变量。

  • 32位的CPU读取一个4字节的int最少需要一次内存访问,最多需要2次访问内存。

C++对象的内存补齐。
- 对象的内存大小为==成员变量的内存之和==,成员函数不占内存,无成员变量的对象大小为==1字节==。
- C++的内存补齐主要遵循两条规则:
1、成员变量的==偏移量==必须为该变量自身大小的整数倍;
2、==对象内存==的大小为最大成员变量大小的==整数倍==;
  • static成员变量==不占==用内存,对象有虚函数时(不论数量)对象的整体内存大小需要==增加一个指针==(虚函数表指针)的内存大小。
  • 另外,宏#pragma pack (n)可以强制设定偏移量为n或自身内存大小中较小值的整数倍,n为1时即设置为无内存补齐。

对齐的优点

  • 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
  • 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。

许多实际的计算机系统对==基本类型数据在内存中存放的位置==有限制,它们会要求这些数据的首地址的值是某个数k(通常它为4或8)的倍数,这就是所谓的内存对齐,而这个k则被称为该数据类型的对齐模数(alignment modulus)。当一种类型S的对齐模数与另一种类型T的对齐模数的比值是大于1的整数,我们就称类型S的对齐要求比T强(严格),而称T比S弱(宽松)。这种强制的要求一来简化了处理器与内存之间传输系统的设计,二来可以提升读取数据的速度。比如这么一种处理器,它每次读写内存的时候都从某个8倍数的地址开始,一次读出或写入8个字节的数据,假如软件能保证double类型的数据都从8倍数地址开始,那么读或写一个double类型数据就只需要一次内存操作。否则,我们就可能需要两次内存操作才能完成这个动作,因为数据或许恰好横跨在两个符合对齐要求的8字节内存块上。某些处理器在数据不满足对齐要求的情况下可能会出错,但是Intel的IA32架构的处理器则不管数据是否对齐都能正确工作。不过Intel奉劝大家,如果想提升性能,那么所有的程序数据都应该尽可能地对齐。
Win32平台下的微软C编译器(cl.exe for 80x86)在默认情况下采用如下的对齐规则: 任何基本数据类型T的对齐模数就是T的大小,即sizeof(T)。比如对于double类型(8字节),就要求该类型数据的地址总是8的倍数,而char类型数据(1字节)则可以从任何一个地址开始。
Linux下的GCC奉行的是另外一套规则(在资料中查得,并未验证,如错误请指正):任何2字节大小(包括单字节吗?)的数据类型(比如short)的对齐模数是2,而其它所有超过2字节的数据类型(比如long,double)都以4为对齐模数。

猜你喜欢

转载自blog.csdn.net/zhc_24/article/details/80267307