網上收集的關于c/c++的基礎知識

推荐给想学C++的朋友一个简单但是完整的学习C++的读书路线图:

C++ Primer-> c++标准程序库-> effective C++-> effective STL->深入探索C++对象模型

C程序常用算法源码
 
算法(Algorithm):计算机解题的基本思想方法和步骤。算法的描述:是对要解决
一个问题或要完成一项任务所采取的方法和步骤的描述,包括需要什么数据
(输入什么数据、输出什么结果)、采用什么结构、使用什么语句以及如何安排这些语句等。
通常使用自然语言、结构化流程图、伪代码等来描述算法。

一、计数、求和、求阶乘等简单算法

  此类问题都要使用循环,要注意根据问题确定循环变量的初值、终值或结束条件,
  更要注意用来表示计数、和、阶乘的变量的初值。

  例:用随机函数产生100个[0,99]范围内的随机整数,统计个位上的数字分别为
  1,2,3,4,5,6,7,8,9,0的数的个数并打印出来。

  本题使用数组来处理,用数组a[100]存放产生的确100个随机整数,
  数组x[10]来存放个位上的数字分别为 1,2,3,4,5,6,7,8,9,0的数的个数。
  即个位是1的个数存放在x[1]中,个位是2的个数存放在x[2]中,……个位是0的个数存放在 x[10]。

void main()
{
  int a[101],x[11],i,p;
 
  for(i=0;i<=11;i++)
    x[i]=0;
   
  for(i=1;i<=100;i++)
  {
    a[i]=rand() % 100;
    printf("%4d",a[i]);
    if(i%10==0)printf("/n");
  }
  
  for(i=1;i<=100;i++)
  {
    p=a[i]%10;
    if(p==0) p=10;
    x[p]=x[p]+1;
  }

  for(i=1;i<=10;i++)
  {
    p=i;
    if(i==10) p=0;
    printf("%d,%d/n",p,x[i]);
  }
  printf("/n");
}

二、求两个整数的最大公约数、最小公倍数

  分析:求最大公约数的算法思想:(最小公倍数=两个整数之积/最大公约数)
  (1) 对于已知两数m,n,使得m>n;
  (2) m除以n得余数r;
  (3) 若r=0,则n为求得的最大公约数,算法结束;否则执行(4);
  (4) m ← n,n ← r,再重复执行(2)。
  例如: 求 m=14 ,n=6 的最大公约数. m n r
  14 6 2
  6 2 0
  void main()
  {
    int nm,r,n,m,t;
    printf("please input two numbers:/n");
    scanf("%d,%d",&m,&n);
    nm=n*m;
    if (m<n)
    {
      t=n;
      n=m;
      m=t;
    }
    
    r=m%n;
    while (r!=0)
    {
      m=n;
      n=r;
      r=m%n;
    }
    printf("最大公约数:%d/n",n);
    printf("最小公倍数:%d/n",nm/n);
  }

三、判断素数

  只能被1或本身整除的数称为素数 基本思想:把m作为被除数,
  将2至sqrt(m)作为除数,如果都除不尽,m就是素数,否则就不是。(可用以下程序段实现)
  
    void main()
    {
      int m,i,k;
      printf("please input a number:/n");
      scanf("%d",&m);
      k=sqrt(m);
      for(i=2;i<k;i++)
        if(m%i==0) break;
      if(i>=k)
        printf("该数是素数");
      else
        printf("该数不是素数");
    }
    
    将其写成一函数,若为素数返回1,不是则返回0
    int prime(int m)
    {
      int i,k;
      k=sqrt(m);
      for(i=2;i<k;i++)
        if(m%i==0) return 0;
      return 1;
    }

四、验证哥德巴赫猜想
    (任意一个大于等于6的偶数都可以分解为两个素数之和)
    基本思想:n为大于等于6的任一偶数,可分解为n1和n2两个数,分别检查
    n1和n2是否为素数,如都是,则为一组解。如n1不是素数,就不必再检查n2是否素数。
    先从n1=3开始,检验n1和n2(n2=N-n1)是否素数。然后使n1+2 再检验n1、n2是否素数,…
    直到n1=n/2为止。

  利用上面的prime函数,验证哥德巴赫猜想的程序代码如下:
    #include "math.h"
    int prime(int m)
    {
      int i,k;
      k=sqrt(m);
      for(i=2;i<k;i++)
        if(m%i==0) break;
      if(i>=k)
        return 1;
      else
        return 0;
    }

    main()
    {
      int x,i;
      printf("please input a even number(>=6):/n");
      scanf("%d",&x);
      if (x<6||x%2!=0)
        printf("data error!/n");
      else
        for(i=2;i<=x/2;i++)
          if (prime(i)&&prime(x-i))
          {
            printf("%d+%d/n",i,x-i);
            printf("验证成功!");
            break;
          }
    }

五、排序问题

  1.选择法排序(升序)
     基本思想:
     1)对有n个数的序列(存放在数组a(n)中),从中选出最小的数,与第1个数交换位置;
     2)除第1 个数外,其余n-1个数中选最小的数,与第2个数交换位置;
     3)依次类推,选择了n-1次后,这个数列已按升序排列。

    程序代码如下:
    void main()
    {
      int i,j,imin,s,a[10];
      printf("/n input 10 numbers:/n");
      for(i=0;i<10;i++)
        scanf("%d",&a[i]);
       
      for(i=0;i<9;i++)
      {
        imin=i;
        for(j=i+1;j<10;j++)
          if(a[imin]>a[j]) imin=j;
         
        if(i!=imin)
        {
          s=a[i];
          a[i]=a[imin];
          a[imin]=s; }
          printf("%d/n",a[i]);
        }
      }
    }

  2.冒泡法排序(升序)
 
     基本思想:(将相邻两个数比较,小的调到前头)
     1)有n个数(存放在数组a(n)中),第一趟将每相邻两个数比较,
        小的调到前头,经n-1次两两相邻比较后,最大的数已“沉底”,
        放在最后一个位置,小数上升“浮起”;
     2)第二趟对余下的n-1个数(最大的数已“沉底”)按上法比较,
        经n-2次两两相邻比较后得次大的数;
     3)依次类推,n个数共进行n-1趟比较,在第j趟中要进行n-j次两两比较。
    程序段如下
    
    void main()
    {
      int a[10];
      int i,j,t;
      printf("input 10 numbers/n");
      for(i=0;i<10;i++)
        scanf("%d",&a[i]);
       
      printf("/n");
      for(j=0;j<=8;j++)
        for(i=0;i<9-j;i++)
          if(a[i]>a[i+1])
          {
            t=a[i];
            a[i]=a[i+1];
            a[i+1]=t;
          }
          
      printf("the sorted numbers:/n");
     
      for(i=0;i<10;i++)
        printf("%d/n",a[i]);
    }

  3.合并法排序(将两个有序数组A、B合并成另一个有序的数组C,升序)
     基本思想:
     1)先在A、B数组中各取第一个元素进行比较,将小的元素放入C数组;
     2)取小的元素所在数组的下一个元素与另一数组中上次比较后较大的元素比较,
        重复上述比较过程,直到某个数组被先排完;
     3)将另一个数组剩余元素抄入C数组,合并排序完成。
    
     程序段如下:
     void main()
     {
        int a[10],b[10],c[20],i,ia,ib,ic;
        printf("please input the first array:/n");
        for(i=0;i<10;i++)
          scanf("%d",&a[i]);
         
        for(i=0;i<10;i++)
          scanf("%d",&b[i]);
         
        printf("/n");
        ia=0;ib=0;ic=0;
        while(ia<10&&ib<10)
        {
          if(a[ia]<b[ib])
          {
            c[ic]=a[ia];
            ia++;
          }
          else
          {
            c[ic]=b[ib];
            ib++;
          }
          ic++;
        }
        
        while(ia<=9)
        {
          c[ic]=a[ia];
         ia++;ic++;
        }
       
        while(ib<=9)
        {
          c[ic]=b[ib];
          b++;ic++;
        }
       
        for(i=0;i<20;i++)
          printf("%d/n",c[i]);
    }

六、查找问题
    1.①顺序查找法(在一列数中查找某数x)
   
    基本思想:一列数放在数组a[1]---a[n]中,待查找的数放在x 中,把x与a数组中的元素
  从头到尾一一进行比较查找。用变量p表示a数组元素下标,p初值为1,使x与a[p]比较,
  如果x不等于a[p],则使 p=p+1,不断重复这个过程;一旦x等于a[p]则退出循环;
  另外,如果p大于数组长度,循环也应该停止。(这个过程可由下语句实现)
  
    void main()
    {
      int a[10],p,x,i;
      printf("please input the array:/n");
      for(i=0;i<10;i++)
        scanf("%d",&a[i]);
       
      printf("please input the number you want find:/n");
      scanf("%d",&x);
      printf("/n");
      p=0;
      while(x!=a[p]&&p<10)
          p++;
      if(p>=10)
        printf("the number is not found!/n");
      else
        printf("the number is found the no%d!/n",p);
    }
    思考:将上面程序改写一查找函数Find,若找到则返回下标值,找不到返回-1
   
    ②基本思想:一列数放在数组a[1]---a[n]中,待查找的关键值为key,
      把key与a数组中的元素从头到尾一一进行比较查找,若相同,查找成功,
      若找不到,则查找失败。(查找子过程如下。index:存放找到元素的下标。)
     
      void main()
      {
        int a[10],index,x,i;
        printf("please input the array:/n");
        for(i=0;i<10;i++)
          scanf("%d",&a[i]);
         
        printf("please input the number you want find:/n");
        scanf("%d",&x);
        printf("/n");
        index=-1;
        for(i=0;i<10;i++)
          if(x==a[i])
          {
            index=i;
            break;
          }
         
        if(index==-1)
          printf("the number is not found!/n");
        else
          printf("the number is found the no%d!/n",index);
      }
     
    2.折半查找法(只能对有序数列进行查找)
    
       基本思想:设n个有序数(从小到大)存放在数组a[1]----a[n]中,
       要查找的数为x。用变量bot、top、mid 分别表示查找数据范围的底部(数组下界)
       顶部(数组的上界)和中间,mid=(top+bot)/2,折半查找的算法如下:
      (1)x=a(mid),则已找到退出循环,否则进行下面的判断;
      (2)x<a(mid),x必定落在bot和mid-1的范围之内,即top=mid-1;
      (3)x>a(mid),x必定落在mid+1和top的范围之内,即bot=mid+1;
      (4)在确定了新的查找范围后,重复进行以上比较,直到找到或者bot<=top。
     
      将上面的算法写成如下程序:
     
      void main()
      {
        int a[10],mid,bot,top,x,i,find;
        printf("please input the array:/n");
        for(i=0;i<10;i++)
          scanf("%d",&a[i]);
         
        printf("please input the number you want find:/n");
       
        scanf("%d",&x);
        printf("/n");
        bot=0;top=9;find=0;
        
        while(bot<top&&find==0)
        {
          mid=(top+bot)/2;
          if(x==a[mid])
          {
            find=1;
            break;
          }
          else
            if(x<a[mid])
              top=mid-1;
            else
              bot=mid+1;
        }
        
        if (find==1)
          printf("the number is found the no%d!/n",mid);
        else
          printf("the number is not found!/n");
     }

  七、插入法

  把一个数插到有序数列中,插入后数列仍然有序

  基本思想:n个有序数(从小到大)存放在数组a(1)—a(n)中,要插入的数x。首先确定x插在数组中的位置P;(可由以下语句实现)
#define N 10
void insert(int a[],int x)
{ int p, i;
p=0;
while(x>a[p]&&p<N)
p++;
for(i=N; i>p; i--)
a[i]=a[i-1];
a[p]=x;
}
main()
{ int a[N+1]={1,3,4,7,8,11,13,18,56,78}, x, i;
for(i=0; i<N; i++) printf("%d,", a[i]);
printf("/nInput x:");
scanf("%d", &x);
insert(a, x);
for(i=0; i<=N; i++) printf("%d,", a[i]);
printf("/n");
}

  八、矩阵(二维数组)运算

(1)矩阵的加、减运算
C(i,j)=a(i,j)+b(i,j) 加法
C(i,j)=a(i,j)-b(i,j) 减法
(2)矩阵相乘
(矩阵A有M*L个元素,矩阵B有L*N个元素,则矩阵C=A*B有M*N个元素)。矩阵C中任一元素 (i=1,2,…,m; j=1,2,…,n)
#define M 2
#define L 4
#define N 3
void mv(int a[M][L], int b[L][N], int c[M][N])
{ int i, j, k;
for(i=0; i<M; i++)
for(j=0; j<N; j++)
{ c[i][j]=0;
for(k=0; k<L; k++)
c[i][j]+=a[i][k]*b[k][j];
}
}
main()
{ int a[M][L]={{1,2,3,4},{1,1,1,1}};
int b[L][N]={{1,1,1},{1,2,1},{2,2,1},{2,3,1}}, c[M][N];
int i, j;
mv(a,b,c);
for(i=0; i<M; i++)
{ for(j=0; j<N; j++)
printf("%4d", c[i][j]);
printf("/n");
}
}
(3)矩阵传置
例:有二维数组a(5,5),要对它实现转置,可用下面两种方式:
#define N 3
void ch1(int a[N][N])
{ int i, j, t;
for(i=0; i<N; i++)
for(j=i+1; j<N; j++)
{ t=a[i][j];
a[i][j]=a[j][i];
a[j][i]=t;
}
}
void ch2(int a[N][N])
{ int i, j, t;
for(i=1; i<N; i++)
for(j= 0; j<i; j++)
{ t=a[i][j];
a[i][j]=a[j][i];
a[j][i]=t;
}
}
main()
{ int a[N][N]={{1,2,3},{4,5,6},{7,8,9}}, i, j;
ch1(a); /*或ch2(a);*/
for(i=0; i<N; i++)
{ for(j=0; j<N; j++)
printf("%4d", a[i][j]);
printf("/n");
}
}
(4)求二维数组中最小元素及其所在的行和列
基本思路同一维数组,可用下面程序段实现(以二维数组a[3][4]为例):
‘变量max中存放最大值,row,column存放最大值所在行列号
#define N 4
#define M 3
void min(int a[M][N])
{ int min, row, column, i, j;
min=a[0][0];
row=0;
column=0;
for(i=0; i<M; i++)
for(j=0; j<N; j++)
if(a[i][j]<min)
{ min=a[i][j];
row=i;
column=j;
}
printf("Min=%d/nAt Row%d,Column%d/n", min, row, column);
}
main()
{ int a[M][N]={{1,23,45,-5},{5,6,-7,6},{0,33,8,15}};
min(a);
}

  九、迭代法

  算法思想:对于一个问题的求解x,可由给定的一个初值x0,根据某一迭代公式得到一个新的值x1,这个新值x1比初值x0更接近要求的值x;再以新值作为初值,即:x1→x0,重新按原来的方法求x1,重复这一过和直到|x1-x0|<ε(某一给定的精度)。此时可将x1作为问题的解。
例:用迭代法求某个数的平方根。 已知求平方根的迭代公式为:
#include<math.h>
float fsqrt(float a)
{ float x0, x1;
x1=a/2;
do{
x0=x1;
x1=0.5*(x0+a/x0);
}while(fabs(x1-x0)>0.00001);
return(x1);
}
main()
{ float a;
scanf("%f", &a);
printf("genhao =%f/n", fsqrt(a));
}

  十、数制转换

  将一个十进制整数m转换成 →r(2-16)进制字符串。

  方法:将m不断除 r 取余数,直到商为零,以反序得到结果。下面写出一转换函数,参数idec为十进制数,ibase为要转换成数的基(如二进制的基是2,八进制的基是8等),函数输出结果是字符串。
char *trdec(int idec, int ibase)
{ char strdr[20], t;
int i, idr, p=0;
while(idec!=0)
{ idr=idec % ibase;
if(idr>=10)
strdr[p++]=idr-10+65;
else
strdr[p++]=idr+48;
idec/=ibase;
}
for(i=0; i<p/2; i++)
{ t=strdr[i];
strdr[i]=strdr[p-i-1];
strdr[p-i-1]=t;
}
strdr[p]=’/0’;
return(strdr);
}
main()
{ int x, d;
scanf("%d%d", &x, &d);
printf("%s/n", trdec(x,d));
}

  十一、字符串的一般处理

  1.简单加密和解密
加密的思想是: 将每个字母C加(或减)一序数K,即用它后的第K个字母代替,变换式公式: c=c+k
例如序数k为5,这时 A→ F, a→f,B→?G… 当加序数后的字母超过Z或z则 c=c+k -26
例如:You are good→ Dtz fwj ltti
解密为加密的逆过程
将每个字母C减(或加)一序数K,即 c=c-k,
例如序数k为5,这时 Z→U,z→u,Y→T… 当加序数后的字母小于A或a则 c=c-k +26
下段程序是加密处理:
#include<stdio.h>
char *jiami(char stri[])
{ int i=0;
char strp[50],ia;
while(stri[i]!=’/0’)
{ if(stri[i]>=’A’&&stri[i]<=’Z’)
{ ia=stri[i]+5;
if (ia>’Z’) ia-=26;
}
else if(stri[i]>=’a’&&stri[i]<=’z’)
{ ia=stri[i]+5;
if (ia>’z’) ia-=26;
}
else ia=stri[i];
strp[i++]=ia;
}
strp[i]=’/0’;
return(strp);
}
main()
{ char s[50];
gets(s);
printf("%s/n", jiami(s));
}
2.统计文本单词的个数
输入一行字符,统计其中有多少个单词,单词之间用格分隔开。
算法思路:
(1)从文本(字符串)的左边开始,取出一个字符;设逻辑量word表示所取字符是否是单词内的字符,初值设为0
(2)若所取字符不是“空格”,“逗号”,“分号”或“感叹号”等单词的分隔符,再判断word是否为1,若word不为1则表是新单词的开始,让单词数num = num +1,让word =1;
(3)若所取字符是“空格”,“逗号”,“分号”或“感叹号”等单词的分隔符, 则表示字符不是单词内字符,让word=0;
(4) 再依次取下一个字符,重得(2)(3)直到文本结束。
下面程序段是字符串string中包含的单词数
#include "stdio.h"
main()
{char c,string[80];
int i,num=0,word=0;
gets(string);
for(i=0;(c=string[i])!='/0';i++)
if(c==' ') word=0;
else if(word==0)
{ word=1;
num++;}
printf("There are %d word in the line./n",num);
}

  十二、穷举法
  
  穷举法(又称“枚举法”)的基本思想是:一一列举各种可能的情况,并判断哪一种可能是符合要求的解,这是一种“在没有其它办法的情况的方法”,是一种最“笨”的方法,然而对一些无法用解析法求解的问题往往能奏效,通常采用循环来处理穷举问题。
例: 将一张面值为100元的人民币等值换成100张5元、1元和0.5元的零钞,要求每种零钞不少于1张,问有哪几种组合?
main()
{ int i, j, k;
printf(" 5元 1元 5角/n");
for(i=1; i<=20; i++)
for(j=1; j<=100-i; j++)
{ k=100-i-j;
if(5*i+1*j+0.5*k==100)
printf(" %3d %3d %3d/n", i, j, k);
}
}

  十三、递归算法
  
  用自身的结构来描述自身,称递归
  
  VB允许在一个Sub子过程和Function过程的定义内部调用自己,即递归Sub子过程和递归Function函数。递归处理一般用栈来实现,每调用一次自身,把当前参数压栈,直到递归结束条件;然后从栈中弹出当前参数,直到栈空。
递归条件:(1)递归结束条件及结束时的值;(2)能用递归形式表示,且递归向终止条件发展。
例:编fac(n)=n! 的递归函数
int fac(int n)
{ if(n==1)
return(1);
else
return(n*fac(n-1));
}
main()
{ int n;
scanf("%d", &n);
printf("n!=%d/n", fac(n));
}
 
*/

/*
签到工作后,用人单位承诺是嵌入式方向,让我好生欢喜,而且用人单位也很负责任的为我们应届毕业生着想,委托了一家在东北地区小有名气的培训机构为我们提供价格不菲的培训, 其中当然少不了C语言,于是特意买了本《C语言的科学与艺术》好好复习C。
        一个博士生讲解的C语言中级教程,虽然没有多少新意,但是总有一些久而荒废的知识,贴出来不断提醒自己“温故知新”。
 

1.关于printf输出函数
       自从学过C++后,就再也没有用过C的printf函数,与std::cout相比printf不能判断输出量的类型,而且对内存的操作,会有危险。

 

printf的原型中参数是常量字符串+...;返回值是整形。

其中的常量字符串中的输出格式需要注意:常用的%d、%f、%s不用多说,但是%g和%%需要说明一下:
    %g表示普通型,数值用%f和%e格式中较短的一个显示,在不能事先确定输出值的情况下,他是最好的输出格式。如定义了float型的变量,如果它是一个整数值,那么以%g格式输出不会显示一堆0,好像是以%d格式输出一般,若是小数值,也不会补充一堆0,让人看得很亲切。
    %%是百分号的输出表示,而不是我们常规思路那样用转义字符/。

精度控制:
    负号:表示数值左对齐,没有负号右对齐;
    宽度:表示输出字段的最小字符数。若要显示的数值所占空间少,则以空格 填充;若数值太大,不能在指定大小的字段中显示,那么扩大字段宽度直到能够容纳这个数值。
    小数点精度:对%g而言,精度参数说明了最大有效位;%f和%e来说精度参数指定了小数点后的位数;%s而言,则表示字符串中显示的位数。
       注意:小数点占一位!
   返回值为int,在windows下平台下表示了printf函数输出的字符个数,或者发生输出错误的相关字节。特别注意一点:/t/n等制表换行的字符,虽然在输出是没有确切的字符显示,但是也算一个字符!

 


2.关于scanf()函数
    这个函数一样没法识别输入量的类型,所以也要在参数表里显示指定类型,如%d,%f等等,但是有一点不同于printf的就是%f,在输出时%f可以输出double和float两种类型,但是输入时,double的位数明显大于float于是如果象这样:
double d;
scanf(“%f”,&d);
printf(“%f”,d);
就会输出%!$%#^@#%一堆不知所云的东西,原因就在于类型的宽度问题,正确的是以%lf输出。

    在windows的32位系统环境下,sizeof 后short 2;int 4;float 4;double 8;但是让我不理解的是long double也是8(怎么也应该比double大呀);
       在linux的32位系统环境下,sizeof后只有long double于win32不同是12。这让我少感欣慰。
       但是还有一点需要注意:输入时可加上宽度限制,但是不可加精度限制;
      如:char c,d;scanf(“%3c,%3c”,&c,&d);printf(“%c,%d”,c,d);若输入abcdef[Enter]则会认定abc中赋给c的是a,def中赋给d的是d。

 

3.getchar和putchar

这两个函数都是一次处理一个字符,以前曾写过:
while(c = getchar())
{putchar(c);
putchar('/n');...}
这次和班里的一位同学讨论说,按照常理思考应该是输入一个字符,马上输出一个字符,但当我们输入一个字符串时,会把这个字符串挨个输出一遍。

   由于这两个函数都可由以上两个函数实现,所以我想是不是putchar中用到了scanf,然后向2那样把输入的字符串放到一个buffer里,然后依次截取下来,轮流处理。
     而若想输入一个字符,马上输出一个字符,可能是需要系统检测keydown的中断,来处理。
     这个问题还不明白,希望如果有哪位高手路过,指点一下!谢谢。

 


  有个哲人曾说过“一知半解是危险的。”最近对这句话的体会越来越深刻。


  林语堂先生说过“只用一样东西,不明白它的道理,是在不高明。”谨记慎行!
 
 
  全局变量&局部变量
        当全局变量和局部变量同名时,会遮蔽全局变量,在函数内引用此变量时会用到同名的局部变量,而不会用到全局变量(如果想引用全局变量需要加上::)。而且局部变量可以相互遮蔽,如if嵌套,for嵌套等。

        这都与局部变量的作用域有关,而作用域又根据各编译器的实现有关,这就联系到编译原理的内容(顺便复习一下)。
        一般而言,标示符的符号表要包含几种属性:名字、类型、存储类别、作用域、可视性、存储分配信息。
1)名字:
      符号表中符号名一般不允许重名,若程序出现重名标示符,则根据语言的定义, 按照该标示符的作用域和可视性规则进行相应的处理。
      若程序允许重载,函数名通过它们的参数个数、类型、返回值来区分。
2)类型:
      决定变量的数据在存储空间的存储格式,还决定了在变量上可施加的运算。
3)存储类别:
       其定义方法有两种形式:关键字指定,变量定义+位置。
       这个属性是编译过程语义处理,检查和存储分配的重要依据,还决定了它的作用 域可视性生命周期等问题。
4)作用域和可视性:
       通常一个变量的作用域就是该变量可以出现的地方;函数形参作为函数内部变量处理;分程序结构本身含有局部变量声明的语句。
5)存储分配:
        编译程序一般根据标示符的存储类别以及他们出现的位置和次序来确定每个变量应分配的存储区及该区域的具体位置。
         静态存储区 公共静态区 公共+外部 整个域作用
                              局部静态区 局部静态     局部
         动态存储区
         标示符在源程序中出现的位置和先后顺序决定了标示符在存储区中的具体位置,即一个偏移量。

关于符号表的建立:
如程序:
main()
{int a = 1; float c =0.1;
 {
   float a =1.0;
   {float x=5.5,b =7.1;}
   {int b =9; c =a+b+c;}
 }
}
1.建立符号表第一层到内存中main的指向;
2.建立符号表第二层到内存中a,c的指向,指向a;
3.建立符号表第三层到内存中a的指向,指向a;(第二层的a与第三层的a不同)
4.建立符号表第四层到内存中x,b的指向,遇到后一个括号}表明本层结束,建立内存中结束项,
   然后从结束项建立   指针,指向上层最后一个元素,即第三层的a,然后删除本层的符号表到内
   存中的指针;
......
n.最后形成的结果是符号表到内存中共有三个指向,分别指向main、a和c的a、a,然后又很多
    结束项指回前三层。
(由于不会在blog里用图形,所以不能更形象地标示出来。等以后整明白的。)

特别一点:
       如果是不同的C文件中,以static方式来声明同名的全局变量,如果要正常运行,要求只能有一个C文件对此变量赋值,此时连接不会出错

 

补充:

Static全局变量和普通全局变量的区别

        全局变量本身就是静态存储方式,静态全局变量当然也是,两者在存储方式上并无区别。
        这两者的区别在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件构成时,非静态的全局
变量在各个文件中都是有效的,而静态全局变量则限制了其作用域,即只在定义该变量的源文件内有效,在同一源
程序的其他源文件中不可使用。
        由于静态全局变量的作用域限制于一个源文件中,只能为该文件内的函数使用,也可避免一些在其他文件中
引起的错误。
 
Static局部变量和普通局部变量的区别
       静态局部变量的存储方式与普通局部变量不同,因此生命周期也不同,后者是动态的,存放于堆栈中。
      全局的在静态区,动态的分配在堆中。
 
Static函数与普通函数的区别
       Static函数在内存中只有一份拷贝,而普通函数每次调用,都会有一份拷贝。

*/
/*

"C之诡谲"C语言之精华总结!
C之诡谲(上)
从研究生二年纪开始学习计算机也差不多两年了,一路走来,有很多的收获,也有不少的遗憾,现在正好有一段闲暇,就想对走过的路留下一些足迹,回忆。每个人都有自己不同的人生,说到这里,就是程序人生了,歌德在《浮士德》中说过:“如果不曾在悲哀中咀嚼过面包,不曾在哭泣中等待过明天,这样的人就不知道你——天的力量。”所以我想记下一些带给我悲哀,带给我哭泣的程序人生。其实学习计算机的基础课程是非常重要的,离散数学,编译原理,操作系统,形式语言……,如果你认真走过了这些路,在以后的日子你会发现你的路会越走越宽,以前的努力和汗水会不断的给你灵感,给你支持,给你前进的武器和勇气。你会发现以后取得的很多成就,不过是朝花夕拾而已!

对于程序语言我喜欢的是C++,它能带给你别的语言无法给予你的无上的智力快感,当然也会给你一门语言所能给你的魔鬼般的折磨。其实Java,C#,Python语言也非常的不错,我也极为喜欢。它们都是非常成功的语言,我从来就不愿意做某一种语言的盲目信仰者,每种语言都有它成功的地方,失败的地方,都有它适合的地方,不如意的地方。所以每一次看到评价语言的文章,我看看,但从来不会发言。

C++的前世是C,而且C所留下的神秘以及精简在C++中是青出于蓝而胜于蓝!C所带给人的困惑以及灵活太多,即使一个有几年经验的高段C程序员仍然有可能在C语言的小水沟里翻船。不过其实C语言真的不难,下面我想指出C语言中最神秘而又诡谲多变的四个地方,它们也继续在C++语言中变幻莫测。

指针,数组,类型的识别,参数可变的函数。

一.指针。

它的本质是地址的类型。在许多语言中根本就没有这个概念。但是它却正是C灵活,高效,在面向过程的时代所向披靡的原因所在。因为C的内存模型基本上对应了现在von Neumann(冯·诺伊曼)计算机的机器模型,很好的达到了对机器的映射。不过有些人似乎永远也不能理解指针【注1】。

注1:Joel Spolsky就是这样认为的,他认为对指针的理解是一种aptitude,不是通过训练就可以达到的http://www.joelonsoftware.com/pr ... /fog0000000073.html

指针可以指向值、数组、函数,当然它也可以作为值使用。

看下面的几个例子:

int* p;//p是一个指针,指向一个整数

int** p;//p是一个指针,它指向第二个指针,然后指向一个整数

int (*pa)[3];//pa是一个指针,指向一个拥有3个整数的数组

int (*pf)();//pf是一个指向函数的指针,这个函数返回一个整数

后面第四节我会详细讲解标识符(identifier)类型的识别。

1.指针本身的类型是什么?

先看下面的例子:int a;//a的类型是什么?

对,把a去掉就可以了。因此上面的4个声明语句中的指针本身的类型为:

int*

int**

int (*)[3]

int (*)()

它们都是复合类型,也就是类型与类型结合而成的类型。意义分别如下:

point to int(指向一个整数的指针)

pointer to pointer to int(指向一个指向整数的指针的指针)

pointer to array of 3 ints(指向一个拥有三个整数的数组的指针)

pointer to function of parameter is void and return value is int (指向一个函数的指针,这个函数参数为空,返回值为整数)

2.指针所指物的类型是什么?

很简单,指针本身的类型去掉 “*”号就可以了,分别如下:

int

int*

int ()[3]

int ()()

3和4有点怪,不是吗?请擦亮你的眼睛,在那个用来把“*”号包住的“()”是多余的,所以:

int ()[3]就是int [3](一个拥有三个整数的数组)

int ()()就是int ()(一个函数,参数为空,返回值为整数)【注2】

注2:一个小小的提醒,第二个“()”是一个运算符,名字叫函数调用运算符(function call operator)。

3.指针的算术运算。

请再次记住:指针不是一个简单的类型,它是一个和指针所指物的类型复合的类型。因此,它的算术运算与之(指针所指物的类型)密切相关。

int a[8];

int* p = a;

int* q = p + 3;

p++;

指针的加减并不是指针本身的二进制表示加减,要记住,指针是一个元素的地址,它每加一次,就指向下一个元素。所以:

int* q = p + 3;//q指向从p开始的第三个整数。

p++;//p指向下一个整数。

double* pd;

……//某些计算之后

double* pother = pd – 2;//pother指向从pd倒数第二个double数。

4.指针本身的大小。

在一个现代典型的32位机器上【注3】,机器的内存模型大概是这样的,想象一下,内存空间就像一个连续的房间群。每一个房间的大小是一个字节(一般是二进制8位)。有些东西大小是一个字节(比如char),一个房间就把它给安置了;但有些东西大小是几个字节(比如double就是8个字节,int就是4个字节,我说的是典型的32位),所以它就需要几个房间才能安置。

注3:什么叫32位?就是机器CPU一次处理的数据宽度是32位,机器的寄存器容量是32位,机器的数据,内存地址总线是32位。当然还有一些细节,但大致就是这样。16位,64位,128位可以以此类推。

这些房间都应该有编号(也就是地址),32位的机器内存地址空间当然也是32位,所以房间的每一个编号都用32位的二进制数来编码【注4】。请记住指针也可以作为值使用,作为值的时候,它也必须被安置在房间中(存储在内存中),那么指向一个值的指针需要一个地址大小来存储,即32位,4个字节,4个房间来存储。

注4:在我们平常用到的32位机器上,绝少有将32位真实内存地址空间全用完的(232 = 4G),即使是服务器也不例外。现代的操作系统一般会实现32位的虚拟地址空间,这样可以方便运用程序的编制。关于虚拟地址(线性地址)和真实地址的区别以及实现,可以参考《Linux源代码情景分析》的第二章存储管理,在互联网上关于这个主题的文章汗牛充栋,你也可以google一下。

但请注意,在C++中指向对象成员的指针(pointer to member data or member function)的大小不一定是4个字节。为此我专门编制了一些程序,发现在我的两个编译器(VC7.1.3088和Dev-C++4.9.7.0)上,指向对象成员的指针的大小没有定值,但都是4的倍数。不同的编译器还有不同的值。对于一般的普通类(class),指向对象成员的指针大小一般为4,但在引入多重虚拟继承以及虚拟函数的时候,指向对象成员的指针会增大,不论是指向成员数据,还是成员函数。【注5】。

注5:在Andrei Alexandrescu的《Modern C++ Design》的5.13节Page124中提到,成员函数指针实际上是带标记的(tagged)unions,它们可以对付多重虚拟继承以及虚拟函数,书上说成员函数指针大小是16,但我的实践告诉我这个结果不对,而且具体编译器实现也不同。一直很想看看GCC的源代码,但由于旁骛太多,而且心不静,本身难度也比较高(这个倒是不害怕^_^),只有留待以后了。

还有一点,对一个类的static member来说,指向它的指针只是普通的函数指针,不是pointer to class member,所以它的大小是4。

5.指针运算符&和*

它们是一对相反的操作,&取得一个东西的地址(也就是指针),*得到一个地址里放的东西。这个东西可以是值(对象)、函数、数组、类成员(class member)。

其实很简单,房间里面居住着一个人,&操作只能针对人,取得房间号码;

*操作只能针对房间,取得房间里的人。

参照指针本身的类型以及指针所指物的类型很好理解。

小结:其实你只要真正理解了1,2,就相当于掌握了指针的牛鼻子。后面的就不难了,指针的各种变化和C语言中其它普通类型的变化都差不多(比如各种转型)。

二.数组。

在C语言中,对于数组你只需要理解三件事。

1.C语言中有且只有一维数组。

所谓的n维数组只是一个称呼,一种方便的记法,都是使用一维数组来仿真的。

C语言中数组的元素可以是任何类型的东西,特别的是数组作为元素也可以。所以int a[3][4][5]就应该这样理解:a是一个拥有3个元素的数组,其中每个元素是一个拥有4个元素的数组,进一步其中每个元素是拥有5个整数元素的数组。

是不是很简单!数组a的内存模型你应该很容易就想出来了,不是吗?:)

2.数组的元素个数,必须作为整数常量在编译阶段就求出来。

int i;

int a;//不合法,编译不会通过。

也许有人会奇怪char str[] = “test”;没有指定元素个数为什么也能通过,因为编译器可以根据后面的初始化字符串在编译阶段求出来,

不信你试试这个:int a[];

编译器无法推断,所以会判错说“array size missing in a”之类的信息。不过在最新的C99标准中实现了变长数组【注6】

注6:如果你是一个好奇心很强烈的人,就像我一样,那么可以查看C99标准6.7.5.2。

3.对于数组,可以获得数组第一个(即下标为0)元素的地址(也就是指针),从数组名获得。

比如int a[5]; int* p = a;这里p就得到了数组元素a[0]的地址。

其余对于数组的各种操作,其实都是对于指针的相应操作。比如a[3]其实就是*(a+3)的简单写法,由于*(a+3)==*(3+a),所以在某些程序的代码中你会看到类似3[a]的这种奇怪表达式,现在你知道了,它就是a[3]的别名。还有一种奇怪的表达式类似a[-1],现在你也明白了,它就是*(a-1)【注7】。

注7:你肯定是一个很负责任的人,而且也知道自己到底在干什么。你难道不是吗?:)所以你一定也知道,做一件事是要付出成本的,当然也应该获得多于成本的回报。

我很喜欢经济学,经济学的一个基础就是做什么事情都是要花成本的,即使你什么事情也不做。时间成本,金钱成本,机会成本,健康成本……可以这样说,经济学的根本目的就是用最小的成本获得最大的回报。

所以我们在自己的程序中最好避免这种邪恶的写法,不要让自己一时的智力过剩带来以后自己和他人长时间的痛苦。用韦小宝的一句话来说:“赔本的生意老子是不干的!”

但是对邪恶的了解是非常必要的,这样当我们真正遇到邪恶的时候,可以免受它对心灵的困扰!

对于指向同一个数组不同元素的指针,它们可以做减法,比如int* p = q+i;p-q的结果就是这两个指针之间的元素个数。i可以是负数。但是请记住:对指向不同的数组元素的指针,这样的做法是无用而且邪恶的!

对于所谓的n维数组,比如int a[2][3];你可以得到数组第一个元素的地址a和它的大小。*(a+0)(也即a[0]或者*a)就是第一个元素,它又是一个数组int[3],继续取得它的第一个元素,*(*(a+0)+0)(也即a[0][0]或者*(*a)),也即第一个整数(第一行第一列的第一个整数)。如果采用这种表达式,就非常的笨拙,所以a[0][0]记法上的简便就非常的有用了!简单明了!

对于数组,你只能取用在数组有效范围内的元素和元素地址,不过最后一个元素的下一个元素的地址是个例外。它可以被用来方便数组的各种计算,特别是比较运算。但显然,它所指向的内容是不能拿来使用和改变的!

关于数组本身大概就这么多,下面简要说一下数组和指针的关系。它们的关系非常暧昧,有时候可以交替使用。

比如 int main(int args, char* argv[])中,其实参数列表中的char* argv[]就是char** argv的另一种写法。因为在C语言中,一个数组是不能作为函数引数(argument)【注8】直接传递的。因为那样非常的损失效率,而这点违背了C语言设计时的基本理念——作为一门高效的系统设计语言。

注8:这里我没有使用函数实参这个大陆术语,而是运用了台湾术语,它们都是argument这个英文术语的翻译,但在很多地方中文的实参用的并不恰当,非常的勉强,而引数表示被引用的数,很形象,也很好理解。很快你就可以像我一样适应引数而不是实参。

dereferance,也就是*运算符操作。我也用的是提领,而不是解引用。

我认为你一定智勇双全:既有宽容的智慧,也有面对新事物的勇气!你不愿意承认吗?:)

所以在函数参数列表(parameter list)中的数组形式的参数声明,只是为了方便程序员的阅读!比如上面的char* argv[]就可以很容易的想到是对一个char*字符串数组进行操作,其实质是传递的char*字符串数组的首元素的地址(指针)。其它的元素当然可以由这个指针的加法间接提领(dereferance)【参考注8】得到!从而也就间接得到了整个数组。

但是数组和指针还是有区别的,比如在一个文件中有下面的定义:

char myname[] = “wuaihua”;

而在另一个文件中有下列声明:

extern char* myname;

它们互相是并不认识的,尽管你的本义是这样希望的。

它们对内存空间的使用方式不同【注9】。

对于char myname[] = “wuaihua”如下

myname

w
u
a
i
h
u
a
/0


对于char* myname;如下表

myname

 


/|/
w
u
a
i
h
u
a
/0


注9:可以参考Andrew Konig的《C陷阱与缺陷》4.5节。

改变的方法就是使它们一致就可以了。

char myname[] = “wuaihua”;

extern char myname[];

或者

char* myname = “wuaihua”;//C++中最好换成const char* myname = “wuaihua”。

extern char* myname;

C之诡谲(下)
三.类型的识别。

基本类型的识别非常简单:

int a;//a的类型是a

char* p;//p的类型是char*

……

那么请你看看下面几个:

int* (*a[5])(int, char*); //#1

void (*b[10]) (void (*)()); //#2

doube(*)() (*pa)[9]; //#3

如果你是第一次看到这种类型声明的时候,我想肯定跟我的感觉一样,就如晴天霹雳,五雷轰顶,头昏目眩,一头张牙舞爪的狰狞怪兽扑面而来。

不要紧(Take it easy)!我们慢慢来收拾这几个面目可憎的纸老虎!

1.C语言中函数声明和数组声明。

函数声明一般是这样int fun(int,double);对应函数指针(pointer to function)的声明是这样:

int (*pf)(int,double),你必须习惯。可以这样使用:

pf = &fun;//赋值(assignment)操作

(*pf)(5, 8.9);//函数调用操作

也请注意,C语言本身提供了一种简写方式如下:

pf = fun;// 赋值(assignment)操作

pf(5, 8.9);// 函数调用操作

不过我本人不是很喜欢这种简写,它对初学者带来了比较多的迷惑。

数组声明一般是这样int a[5];对于数组指针(pointer to array)的声明是这样:

int (*pa)[5]; 你也必须习惯。可以这样使用:

pa = &a;// 赋值(assignment)操作

int i = (*pa)[2]//将a[2]赋值给i;


2.有了上面的基础,我们就可以对付开头的三只纸老虎了!:)

这个时候你需要复习一下各种运算符的优先顺序和结合顺序了,顺便找本书看看就够了。

#1:int* (*a[5])(int, char*);

首先看到标识符名a,“[]”优先级大于“*”,a与“[5]”先结合。所以a是一个数组,这个数组有5个元素,每一个元素都是一个指针,指针指向“(int, char*)”,对,指向一个函数,函数参数是“int, char*”,返回值是“int*”。完毕,我们干掉了第一个纸老虎。:)

#2:void (*b[10]) (void (*)());

b是一个数组,这个数组有10个元素,每一个元素都是一个指针,指针指向一个函数,函数参数是“void (*)()”【注10】,返回值是“void”。完毕!

注10:这个参数又是一个指针,指向一个函数,函数参数为空,返回值是“void”。

#3. doube(*)() (*pa)[9];

pa是一个指针,指针指向一个数组,这个数组有9个元素,每一个元素都是“doube(*)()”【也即一个指针,指向一个函数,函数参数为空,返回值是“double”】。

现在是不是觉得要认识它们是易如反掌,工欲善其事,必先利其器!我们对这种表达方式熟悉之后,就可以用“typedef”来简化这种类型声明。

#1:int* (*a[5])(int, char*);

typedef int* (*PF)(int, char*);//PF是一个类型别名【注11】。

PF a[5];//跟int* (*a[5])(int, char*);的效果一样!

注11:很多初学者只知道typedef char* pchar;但是对于typedef的其它用法不太了解。Stephen Blaha对typedef用法做过一个总结:“建立一个类型别名的方法很简单,在传统的变量声明表达式里用类型名替代变量名,然后把关键字typedef加在该语句的开头”。可以参看《程序员》杂志2001.3期《C++高手技巧20招》。

#2:void (*b[10]) (void (*)());

typedef void (*pfv)();

typedef void (*pf_taking_pfv)(pfv);

pf_taking_pfv b[10]; //跟void (*b[10]) (void (*)());的效果一样!

#3. doube(*)() (*pa)[9];

typedef double(*PF)();

typedef PF (*PA)[9];

PA pa; //跟doube(*)() (*pa)[9];的效果一样!


3.const和volatile在类型声明中的位置

在这里我只说const,volatile是一样的【注12】!

注12:顾名思义,volatile修饰的量就是很容易变化,不稳定的量,它可能被其它线程,操作系统,硬件等等在未知的时间改变,所以它被存储在内存中,每次取用它的时候都只能在内存中去读取,它不能被编译器优化放在内部寄存器中。

类型声明中const用来修饰一个常量,我们一般这样使用:const在前面

const int;//int是const

const char*;//char是const

char* const;//*(指针)是const

const char* const;//char和*都是const

对初学者,const char*;和 char* const;是容易混淆的。这需要时间的历练让你习惯它。

上面的声明有一个对等的写法:const在后面

int const;//int是const

char const*;//char是const

char* const;//*(指针)是const

char const* const;//char和*都是const

第一次你可能不会习惯,但新事物如果是好的,我们为什么要拒绝它呢?:)const在后面有两个好处:

A. const所修饰的类型是正好在它前面的那一个。如果这个好处还不能让你动心的话,那请看下一个!

B.我们很多时候会用到typedef的类型别名定义。比如typedef char* pchar,如果用const来修饰的话,当const在前面的时候,就是const pchar,你会以为它就是const char* ,但是你错了,它的真实含义是char* const。是不是让你大吃一惊!但如果你采用const在后面的写法,意义就怎么也不会变,不信你试试!

不过,在真实项目中的命名一致性更重要。你应该在两种情况下都能适应,并能自如的转换,公司习惯,商业利润不论在什么时候都应该优先考虑!不过在开始一个新项目的时候,你可以考虑优先使用const在后面的习惯用法。


四.参数可变的函数

C语言中有一种很奇怪的参数“…”,它主要用在引数(argument)个数不定的函数中,最常见的就是printf函数。

printf(“Enjoy yourself everyday!/n”);

printf(“The value is %d!/n”, value);

……

你想过它是怎么实现的吗?

1. printf为什么叫printf?

不管是看什么,我总是一个喜欢刨根问底的人,对事物的源有一种特殊的癖好,一段典故,一个成语,一句行话,我最喜欢的就是找到它的来历,和当时的意境,一个外文翻译过来的术语,最低要求我会尽力去找到它原本的外文术语。特别是一个字的命名来历,我一向是非常在意的,中国有句古话:“名不正,则言不顺。”printf中的f就是format的意思,即按格式打印【注13】。

注13:其实还有很多函数,很多变量,很多命名在各种语言中都是非常讲究的,你如果细心观察追溯,一定有很多乐趣和满足,比如哈希表为什么叫hashtable而不叫hashlist?在C++的SGI STL实现中有一个专门用于递增的函数iota(不是itoa),为什么叫这个奇怪的名字,你想过吗?

看文章我不喜欢意犹未尽,己所不欲,勿施于人,所以我把这两个答案告诉你:

(1)table与list做为表讲的区别:

table:

-------|--------------------|-------

item1 | kadkglasgaldfgl | jkdsfh

-------|--------------------|-------

item2 | kjdszhahlka | xcvz

-------|--------------------|-------

list:

****

***

*******

*****

That's the difference!

如果你还是不明白,可以去看一下hash是如何实现的!

(2)The name iota is taken from the programming language APL.

而APL语言主要是做数学计算的,在数学中有很多公式会借用希腊字母,

希腊字母表中有这样一个字母,大写为Ι,小写为ι,

它的英文拼写正好是iota,这个字母在θ(theta)和κ(kappa)之间!

你可以http://www.wikipedia.org/wiki/APL_programming_language

下面有一段是这样的:

APL is renowned for using a set of non-ASCII symbols that are an extension of traditional arithmetic and algebraic notation. These cryptic symbols, some have joked, make it possible to construct an entire air traffic control system in two lines of code. Because of its condensed nature and non-standard characters, APL has sometimes been termed a "write-only language", and reading an APL program can feel like decoding an alien tongue. Because of the unusual character-set, many programmers used special APL keyboards in the production of APL code. Nowadays there are various ways to write APL code using only ASCII characters.

在C++中有函数重载(overload)可以用来区别不同函数参数的调用,但它还是不能表示任意数量的函数参数。

在标准C语言中定义了一个头文件专门用来对付可变参数列表,它包含了一组宏,和一个va_list的typedef声明。一个典型实现如下【注14】:

typedef char* va_list;

#define va_start(list) list = (char*)&va_alist

#define va_end(list)

#define va_arg(list, mode)
((mode*) (list += sizeof(mode)))[-1]

注14:你可以查看C99标准7.15节获得详细而权威的说明。也可以参考Andrew Konig的《C陷阱与缺陷》的附录A。

ANSI C还提供了vprintf函数,它和对应的printf函数行为方式上完全相同,只不过用va_list替换了格式字符串后的参数序列。至于它是如何实现的,你在认真读完《The C Programming Language》后,我相信你一定可以do it yourself!

使用这些工具,我们就可以实现自己的可变参数函数,比如实现一个系统化的错误处理函数error。它和printf函数的使用差不多。只不过将stream重新定向到stderr。在这里我借鉴了《C陷阱与缺陷》的附录A的例子。

实现如下:

#include

#include

void error(char* format, …)

{

va_list ap;

va_start(ap, format);

fprintf(stderr, “error: “);

vfprintf(stderr, format, ap);

va_end(ap);

fprintf(stderr, “/n”);

exit(1);

}

你还可以自己实现printf:

#include

int printf(char* format, …)

{

va_list ap;

va_start(ap, format);

int n = vprintf(format, ap);

va_end(ap);

return n;

}

我还专门找到了VC7.1的头文件看了一下,发现各个宏的具体实现还是有区别的,跟很多预处理(preprocessor)相关。其中va_list就不一定是char*的别名。

typedef struct {

char *a0; /* pointer to first homed integer argument */
int offset; /* byte offset of next parameter */
} va_list;

其它的定义类似。


经常在Windows进行系统编程的人一定知道函数调用有好几种不同的形式,比如__stdcall,__pascal,__cdecl。在Windows下_stdcall,__pascal是一样的,所以我只说一下__stdcall和__cdecl的区别。

(1)__stdcall表示被调用端自身负责函数引数的压栈和出栈。函数参数个数一定的函数都是这种调用形式。

例如:int fun(char c, double d),我们在main函数中使用它,这个函数就只管本身函数体的运行,参数怎么来的,怎么去的,它一概不管。自然有main负责。不过,不同的编译器的实现可能将参数从右向左压栈,也可能从左向右压栈,这个顺序我们是不能加于利用的【注15】。

注15:你可以在Herb Sutter的《More Exceptional C++》中的条款20:An Unmanaged Pointer Problem, Part 1:Parameter Evaluation找到相关的细节论述。

(2)__cdecl表示调用端负责被调用端引数的压栈和出栈。参数可变的函数采用的是这种调用形式。

为什么这种函数要采用不同于前面的调用形式呢?那是因为__stdcall调用形式对它没有作用,被调用端根本就无法知道调用端的引数个数,它怎么可能正确工作?所以这种调用方式是必须的,不过由于参数参数可变的函数本身不多,所以用的地方比较少。

对于这两种方式,你可以编制一些简单的程序,然后反汇编,在汇编代码下面你就可以看到实际的区别,很好理解的!

重载函数有很多匹配(match)规则调用。参数为“…”的函数是匹配最低的,这一点在Andrei Alexandrescu的惊才绝艳之作《Modern C++ Design》中就有用到,参看Page34-35,2.7“编译期间侦测可转换性和继承性”。


后记:

C语言的细节肯定不会只有这么多,但是这几个出现的比较频繁,而且在C语言中也是很重要的几个语言特征。如果把这几个细节彻底弄清楚了,C语言本身的神秘就不会太多了。

C语言本身就像一把异常锋利的剪刀,你可以用它做出非常精致优雅的艺术品,也可以剪出一些乱七八糟的废纸片。能够将一件武器用到出神入化那是需要时间的,需要多长时间?不多,请你拿出一万个小时来,英国Exter大学心理学教授麦克.侯威专门研究神童和天才,他的结论很有意思:“一般人以为天才是自然而生、流畅而不受阻的闪亮才华,其实,天才也必须耗费至少十年光阴来学习他们的特殊技能,绝无例外。要成为专家,需要拥有顽固的个性和坚持的能力……每一行的专业人士,都投注大量心血,培养自己的专业才能。”【注16】

注16:台湾女作家、电视节目主持人吴淡如《拿出一万个小时来》。《读者》2003.1期。“不用太努力,只要持续下去。想拥有一辈子的专长或兴趣,就像一个人跑马拉松赛一样,最重要的是跑完,而不是前头跑得有多快。”

推荐两本书:

K&R的《The C Programming language》,Second Edition。

Andrew Konig的《C陷阱与缺陷》。本文从中引用了好几个例子,一本高段程序员的经验之谈。

但是对纯粹的初学者不太合适,如果你有一点程序设计的基础知识,花一个月的时间好好看看这两本书,C语言本身就不用再花更多的精力了
*/

/*
刚看完书,说什么也得写点东西总结一下,不自量力挑我认为最困难的部分吧
数组
给我印象最深刻的就是两个字:“左值”,当然还得加上两个字:“不是”。数组名是一个指针,指针是左值,但是数组名在C中不是一个左值,所以在C中不能出现在赋值运算的左边。
要牢记这一点,所以不可以出现把一个数组名当整个数组赋值,要赋值的话,没什么好办法,循环吧,或者用一些库里的函数,但是在用函数的时候,要牢记数组的长度,要提防越界,像我们不经常使用的fscanf这样的函数,有缓冲区,就要顾及他的溢出问题,可以制定一个字段宽度,表示要读入的最大字符数。

要确定数组的元素个数可以用到sizeof如:sizeof(a)/sizeof(a[0]),用数组中的第一个元素的大小去除整个数组的大小。

还有一点需要注意的是:值传递。当用一个简单变量调用函数时,函数会受到被调用的参数的一个拷贝,这就是参数传递,而当一个数组参数的函数被调用时,形参和实参的关系就发生了变化,变成了值传递。(不会在blog里插入图片-_-b)用语言来描述其原因的话,就是因为指针,当数组作为参数传递给函数时,只有数组的基址——数组首元素——数组名传递给函数,作为函数局部栈的地址,这个名字存放了实际数组的地址,所以即便是想象的“值赋值”(当然不能,原因如上),内容也是一样的。如果选择在函数范围内定义的局部数组中的一个元素,那么把偏移量+基地址一样会在实际数组中操作,最终结果是函数声明中定义的形参与函数调用时使用的数组实参是相同的一块内存,而不是一个拷贝。


指针
无疑,指针是C中最精髓的部分,因为指针可以在初始化后,可以同时拥有所指变量的两样东西——值和地址。这就给我们写程序时很大的空间,可以直接与内存对话!这也同样引出了千奇百怪的错误,不知道该怎么表达,其实最根本的是要明白我们在使用指针的时候,知道我们使用的究竟是她的哪个性质!是值?还是地址?于此对应的,指针有两种最基本的操作:
                      一个是取地址&,主要用于初始化时的赋值操作。&必须是左值。
                      一个是取指向的值*,*可以取任意指向的值,返回其左值。
对指针的操作犹如打太极一般,有很多招式,但又归于一招。最基础的是分清指针赋值和值赋值:
             p1=p2;指针赋值,是p1和p2指向同一位置。
             *p1=*p2;值赋值,把p2为地址的内存中的内容赋到p1为地址的内存的。
注意:指针也是有地址的,它本身也需要在内存中开辟一块存储,这块存储空间里是他所指变量的地址,然后根据这个地址,可以找到所指变量的值!
指针可以被运算,但要注意的是指针所指向对象的类型,指针都是一样的——4,而他的指向的解析方式是不同的,所以同样的形式会有不同的运算方法,如:p++,对于int型和对于double型所跨越的实际地址是不同的!


指针和数组
我们使用指针的时候,其作用和其他变量相似,可以把他的行为与基本类型划等号。但是如果是数组,就不同了,数组声明后保有很多内存单元,每个元素都有一个内存单元,数组名不与某个单独的内存单元相对应,而是和整个内存单元集合相对应,所以这一点和普通变量不同。
当变量做最普通的声明时,会体现数组和指针最关键的区别:int array【5】;和int * p;内存的分配!这样指针是不分配内存的 ,但是数组分配!
*/

/*
一、文件包含
#include <头文件名称>
#include "头文件名称"
第一种形式 : 用来包含开发环境提供的库头文件,它指示编译预处理器在开发环境设定的搜索路径中查找所需的头文件
第二种形式 : 用来包含自己编写的头文件,它指示编译预处理器首先在当前工作目录下搜索头文件,如果找不到再到开发环境设定的路径中查找。
 
内部包含卫哨和外部包含卫哨
在头文件里面使用内部包含卫哨,就是使用一种标志宏,可以放心的在同一个编译单元及其包含的头文件中多次包含同一个头文件而不会造成重复包含。如:
#ifndef _STDDEF_H_INCLUDED_
#define _STDDEF_H_INCLUDED_
...... //头文件的内容
#endif
 
当包含一个头文件的时候,如果能够始终如一地使用外部包含卫哨,可以显著地提高编译速度,因为当一个头文件被一个源文件反复包含多次时,可以避免多次查找和打开头文件地操作。如:
#if !defined(_INCLUDED_STDDEF_H_)
#include <stddef.h>
#define _INCLUDED_STDDEF_H_
#endif
建议外部包含卫哨和内部包含卫哨使用同一个标志宏,这样可以少定义一个标志宏。如:
#if !defined_STDDEF_H_INCLUDED_
#include <stddef.h>
#endif
 
头文件包含的合理顺序
在头文件中:
1、包含当前工程中所需的自定义头文件
2、包含第三方程序库的头文件
3、包含标准头文件
在源文件中:
1、包含该源文件对应的头文件
2、包含当前工程中所需的自定义头文件
3、包含第三方程序库的头文件
4、包含标准头文件
 
 
 
 
避免重定义
  如果把一个struct定义放在一个头文件中,就有可能在一个编译程序中多次包含这个头文件。编译器认为重定义是一个错误。如下面的例子:
  // file : type.h
  struct type01
  {
  int a,b,c;
  };
  // file : a.h
  #include "type.h"
  ……
  // file : b.h
  #include "type.h"
  ……
  // file main.cpp
  #include "a.h"
  #include "b.h"
  int main(void)
  {
  ……
  }
  编译程序,编译器给出以下的错误提示:
  error C2011: “type01” : “struct”类型重定义
  原因是头文件type.h定义了一个struct类型type01,头文件a.h和b.h都包含了头文件type.h。而在main.cpp文件里却同时包含了头文件a.h和b.h。因此出现了重定义的错误。
  可以通过像以下那样改写type.h文件,从而避免重定义错误:
  // file : type.h
  #ifndef __TYPE_H__ // 如果标记没被设置
  #define __TYPE_H__ // 设置标记
  struct type01
  {
  int a,b,c;
  };
  #endif // End of __TYPE_H__
  通过这样改写type.h文件后,程序可以顺利编译过去了。
  我们是通过测试预处理器的标记来检查type.h头文件是否已经包含过了。如果这个标记没有设置,表示这个头文件没有被包含过,则应该设计标记。反之,如果这个标记已经设置,则表示这个头文件已经被包含,所以应该忽略。
*/

/*
C++为类中提供类成员的初始化列表

类对象的构造顺序是这样的:
1.分配内存,调用构造函数时,隐式/显示的初始化各数据成员
2.进入构造函数后在构造函数中执行一般计算

使用初始化列表有两个原因:

1.必须这样做:
        如果我们有一个类成员,它本身是一个类或者是一个结构,而且这个成员它只有一个带参数的构造函数,而没有默认构造函数,这时要对这个类成员进行初始化,就必须调用这个类成员的带参数的构造函数,如果没有初始化列表,那么他将无法完成第一步,就会报错。

class ABC
...{
public:
         ABC(int x,int y,int z);
private:
         int a;
         int b;
         int c;
};
class MyClass
...{
public:
        MyClass():abc(1,2,3)...{}
private:
        ABC abc;
};

        因为ABC有了显示的带参数的构造函数,那么他是无法依靠编译器生成无参构造函数的,所以没有三个int型数据,就无法创建ABC的对象。
        ABC类对象是MyClass的成员,想要初始化这个对象abc,那就只能用成员初始化列表,没有其他办法将参数传递给ABC类构造函数。
        另一种情况是这样的:当类成员中含有一个const对象时,或者是一个引用时,他们也必须要通过成员初始化列表进行初始化,因为这两种对象要在声明后马上初始化,而在构造函数中,做的是对他们的赋值,这样是不被允许的。

2.效率要求这样做:
       类对象的构造顺序显示,进入构造函数体后,进行的是计算,是对他们的赋值操作,显然,赋值和初始化是不同的,这样就体现出了效率差异,如果不用成员初始化类表,那么类对自己的类成员分别进行的是一次隐式的默认构造函数的调用,和一次复制操作符的调用,如果是类对象,这样做效率就得不到保障。

注意:构造函数需要初始化的数据成员,不论是否显示的出现在构造函数的成员初始化列表中,都会在该处完成初始化,并且初始化的顺序和其在声明时的顺序是一致的,与列表的先后顺序无关,所以要特别注意,保证两者顺序一致才能真正保证其效率。

为了说明清楚,假设有这样一个类:
class foo{
   private :
    int a, b;
};
1、foo(){}和foo(int i = 0){}都被认为是默认构造函数,因为后者是默认参数。两者不能同时出现。
2、构造函数列表的初始化方式不是按照列表的的顺序,而是按照变量声明的顺序。比如foo里面,a在b之前,那么会先构造a再构造b。所以无论 foo():a(b + 1), b(2){}还是foo():b(2),a(b+1){}都不会让a得到期望的值。如果先声明b再声明a则会更好。
3、构造函数列表能够对const成员初始化。比如foo里面有一个int const c;则foo(int x) : c(x){}可以让c值赋成x。不过需要注意的是,c必须在每个构造函数(如果有多个)都有值。
4、在继承里面,只有初始化列表可以构造父类的private成员。比如说
class child : public foo{
}
foo里面的构造函数是这样写的:foo (int x) { a = x; }.
而在child里面写child(int x){ foo(x); }是通过不了编译的。只有把父类初始化改为foo(int x) : a(x){}而子类构造写作child (int x) : foo(x){}才可以。

另一篇关于初始化列表的文章:

C++初始化类的成员,不但可以用构造函数(constructor)完成,而且可以用初始化类成员列表来完成。MFC大量用到此方法。例如有些初学者可能不大理解如下代码:
class A
{
  public:
    int member_var; //成员变量
    A();            //构造函数
}
A::A():member_var(0)
{
}
 
他们觉得这个构造函数的定义应该只能这样写:
A::A()
{
   member_var=1;
}
 
其实两种方法都可。但是有些情况下,只能用第一种,而且通常情况下用第一种也会效率高些。
 
其实,第一种方法是真正的初始化(initialization),而在构造函数内实现的“=”操作其实是赋值(assign)。这两种方法的一切区别从这儿开始。区别大概如下:
 
我们知道普通变量编译器都会默认的替你初始化。他们既能初始化,也能被赋值的,而常量(const)按照其意思只能被初始化,不能赋值。否则与变量就无区别了。所以常量成员(const member)只能用成员初始化列表来完成他们的“初始化”,而不能在构造函数内为他们“赋值”。
我们知道类的对象的初始化其实就是调用他的构造函数完成,如果没有写构造函数,编译器会为你默认生成一个。如果你自定义了带参数的构造函数,那么编译器将不生成默认构造函数。这样这个类的对象的初始化必须有参数。如果这样的类的对象来做另外某个类的成员,那么为了初始化这个成员,你必须为这个类的对象的构造函数传递一个参数。同样,如果你在包含它的这个类的构造函数里用“=”,其实是为这个对象“赋值”而非“初始化”它。所以一个类里的所有构造函数都是有参数的,那么这样的类如果做为别的类的成员变量,你必须显式的初始化它,你也是只能通过成员初始化列表来完成初始化。例如:
class B
{
......
}

class A
{
  public:
  B member_b;
  A();
}
A::A():B(...) //你必须显式初始化它,因为他的所有构造函数
              //都是有参数的,之后才能被赋值。
{
  B=...; //因为如上所写,已经初始化了,才能被赋值,否则错误。
}

 

——————————————————————————————————————

初始化顺序:

class test
{

       const int a;

       std:string str;

       object o;

       test():str(“df”),o(null),a(0)

{

}    

};

 

黄色的既是初始化列表,他们会在构造函数正式调用前被调用,且他们的初始化顺序并不是根据 初始化列表中出现的顺序,而是他们声明的顺序来初始化。如上:

初始化顺序是:a, str, o;

一般用于初始化 常量类型,静态类型的数据,或者不能独立存在的数据
*/

/*
C++的11个注意要点
 
 
 
下面的这些要点是对所有的C++程序员都适用的。我之所以说它们是最重要的,是因为这些要点中提到的是你通常在C++书中或网站上无法找到的。如:指向成员的指针,这是许多资料中都不愿提到的地方,也是经常出错的地方,甚至是对一些高级的C++程序员也是如此。
  这里的要点不仅仅是解释怎样写出更好的代码,更多的是展现出语言规则里面的东西。很显然,它们对C++程序员来说是永久的好资料。我相信这一篇文章会使你收获不小。

  首先,我把一些由不同层次的C++程序员经常问的问题归到一起。我惊奇的发现有很多是有经验的程序员都还没意识到 .h 符号是否还应该出现在标准头文件中。


要点1: <iostream.h> 还是 <iostream>?

  很多C++程序员还在使用<iostream.h>而不是用更新的标准的<iostream>库。这两者都有什么不同呢?首先,5年前我们就开始反对把.h符号继续用在标准的头文件中。继续使用过时的规则可不是个好的方法。从功能性的角度来讲,<iostream>包含了一系列模板化的I/O类,相反地<iostream.h>只仅仅是支持字符流。另外,输入输出流的C++标准规范接口在一些微妙的细节上都已改进,因此,<iostream>和<iostream.h>在接口和执行上都是不同的。最后,<iostream>的各组成都是以STL的形式声明的,然而<iostream.h>的各组成都是声明成全局型的。

  因为这些实质上的不同,你不能在一个程序中混淆使用这两个库。做为一种习惯,在新的代码中一般使用<iostream>,但如果你处理的是过去编写的代码,为了继承可以用继续用<iostream.h>旧保持代码的一致性。  


要点2:用引用传递参数时应注意的地方

  在用引用传递参数时,最好把引用声明为const类型。这样做的好处是:告诉程序不能修改这个参数。在下面的这个例子中函数f()就是传递的引用:

void f(const int & i);
int main()
{
 f(2); /* OK */
}

  这个程序传递一个参数2给f()。在运行时,C++创建一个值为2的int类型的临时变量,并传递它的引用给f().这个临时变量和它的引用从f()被调用开始被创建并存在直到函数返回。返回时,就被马上删除。注意,如果我们不在引用前加上const限定词,则函数f()可能会更改它参数的值,更可能会使程序产生意想不到的行为。所以,别忘了const。

  这个要点也适用于用户定义的对象。你可以给临时对象也加上引用如果是const类型:

struct A{};
void f(const A& a);
int main()
{
 f(A()); // OK,传递的是一个临时A的const引用
}


要点3:“逗号分离”表达形式

 “逗号分离”表达形式是从C继承来的,使用在for-和while-循环中。当然,这条语法规则被认为是不直观的。首先,我们来看看什么是“逗号分离”表达形式。

  一个表达式由一个或多个其它表达式构成,由逗号分开,如:

 if(++x, --y, cin.good()) //三个表达式
  这个if条件包含了三个由逗号分离的表达式。C++会计算每个表达式,但完整的“逗号分离”表达式的结果是最右边表达式的值。因此,仅当cin.good()返回true时,if条件的值才是true。下面是另一个例子:
int j=10;
int i=0;
while( ++i, --j)
{
 //直到j=0时,循环结束,在循环时,i不断自加
}

要点4,使用全局对象的构造函数在程序启动前调用函数

  有一些应用程序需要在主程序启动前调用其它函数。如:转态过程函数、登记功能函数都是必须在实际程序运行前被调用的。最简单的办法是通过一个全局对象的构造函数来调用这些函数。因为全局对象都是在主程序开始前被构造,这些函数都将会在main()之前返回结果。如:
class Logger
{

 public:
 Logger()
  {
   activate_log();//译者注:在构造函数中调用你需要先运行的函数
  }
};
Logger log; //一个全局实例

int main()
{
 record * prec=read_log();//译者注:读取log文件数据
 //.. 程序代码
}


  全局对象log在main()运行之前被构造,log调用了函数activate_log()。从而,当main()开始执行时,它就可以从log文件中读取数据。


  毫无疑问地,在C++编程中内存管理是最复杂和最容易出现bug的地方。直接访问原始内存、动态分配存储和最大限度的发挥C++指令效率,都使你必须尽力避免

有关内存的bug。
  
要点5:避免使用复杂构造的指向函数的指针

  指向函数的指针是C++中可读性最差的语法之一。你能告诉我下面语句的意思吗?

void (*p[10]) (void (*)());
  P是一个“由10个指针构成的指向一个返回void类型且指向另一个无返回和无运算的函数的数组”。这个麻烦的语法真是让人难以辨认,不是吗?你其实可以简单的通过typedef来声明相当于上面语句的函数。首先,使用typedef声明“指向一个无返回和无运算的函数的指针”:
typedef void (*pfv)();
  接着,声明“另一个指向无返回且使用pfv的函数指针”:
typedef void (*pf_taking_pfv) (pfv);
  现在,声明一个由10个上面这样的指针构成的数组:
pf_taking_pfv p[10];
  与void (*p[10]) (void (*)())达到同样效果。但这样是不是更具有可读性
了!

要点6:指向成员的指针

  一个类有两种基本的成员:函数成员和数据成员。同样的,指向成员的指针也有两种:指向函数成员的指针和指向数据成员的指针。后则其实并不常用,因为类一般是不含有公共数据成员的,仅当用在继承用C写的代码时协调结构(struct)和类(class)时才会用到。

  指向成员的指针是C++语法中最难以理解的构造之一,但是这也是一个C++最强大的特性。它可以让你调用一个类的函数成员而不必知道这个函数的名字。这一个非常敏捷的调用工具。同样的,你也可以通过使用指向数据成员的指针来检查并改变这个数据而不必知道它的成员名字。

  指向数据成员的指针

  尽管刚开始时,指向成员的指针的语法会使你有一点点的迷惑,但你不久会发现它其实同普通的指针差不多,只不过是*号的前面多了::符号和类的名字,例:定义一个指向int型的指针:


int * pi;
  定义一个指向为int型的类的数据成员:
int A::*pmi; //pmi是指向类A的一个int型的成员
  你可以这样初始化它:
class A
{
 public:
 int num;
 int x;
};
int A::*pmi = & A::num;
  上面的代码是声明一个指向类A的一个int型的num成员并将它初始化为这个num成员的地址.通过在pmi前面加上*你就可以使用和更改类A的num成员的值:
A a1, a2;
int n=a1.*pmi; //把a1.num赋值给n
a1.*pmi=5; // 把5赋值给a1.num
a2.*pmi=6; // 把6赋值给6a2.num

  如果你定义了一个指向类A的指针,那么上面的操作你必须用 ->*操作符代替:
A * pa=new A;
int n=pa->*pmi;
pa->*pmi=5;

  指向函数成员的指针

  它由函数成员所返回的数据类型构成,类名后跟上::符号、指针名和函数的参数列表。举个例子:一个指向类A的函数成员(该函数返回int类型)的指针:

class A
{
 public:
 int func ();
};
int (A::*pmf) ();

  上面的定义也就是说pmf是一个指向类A的函数成员func()的指针.实际上,这个指针和一个普通的指向函数的指针没什么不同,只是它包含了类的名字和::符号。你可以在在任何使用*pmf的地方调用这个函数
func():
pmf=&A::func;
A a;
(a.*pmf)(); //调用a.func()
  如果你先定义了一个指向对象的指针,那么上面的操作要用->*代替:
A *pa=&a;
(pa->*pmf)(); //调用pa->func()
  指向函数成员的指针要考虑多态性。所以,当你通过指针调用一个虚函数成员时,这个调用将会被动态回收。另一个需要注意的地方,你不能取一个类的构造函数和析构函数的地址。

要点7、避免产生内存碎片


  经常会有这样的情况:你的应用程序每运行一次时就因为程序自身缺陷而产生内存漏洞而泄漏内存,而你又在周期性地重复着你的程序,结果可想而知,它也会使系统崩溃。但怎样做才能预防呢?首先,尽量少使用动态内存。在大多数情况下,你可能使用静态或自动存储或者是STL容器。第二,尽量分配大块的内存而不是一次只分配少量内存。举个例子:一次分配一个数组实例所需的内存,而不是一次只分配一个数组元素的内存。

要点8、是delete还是delete[]

  在程序员中有个荒诞的说法:使用delete来代替delete[]删除数组类型时是可以的!
  举个例子吧:

 int *p=new int[10];
 delete p; //错误,应该是:delete[] p
  上面的程序是完全错误的。事实上,在一个平台上使用delete代替delete[]的应用程序也许不会造成系统崩溃,但那纯粹是运气。你不能保证你的应用程序是不是会在另一个编译器上编译,在另一个平台上运行,所以还是请使用delete[]。

要点9、优化成员的排列

  一个类的大小可以被下面的方式改变:

struct A

{
 bool a;
 int b;
 bool c;
}; //sizeof (A) == 12

  在我的电脑上sizeof (A) 等于12。这个结果可能会让你吃惊,因为A的成员总数是6个字节:1+4+1个字节。那另6字节是哪儿来的?编译器在每个bool成员后面都插入了3个填充字节以保证每个成员都是按4字节排列,以便分界。你可以减少A的大小,通过以下方式:

struct B
{
 bool a;
 bool c;
 int b;
}; // sizeof (B) == 8

  这一次,编译器只在成员c后插入了2个字节。因为b占了4个字节,所以就很自然地把它当作一个字的形式排列,而a和c的大小1+1=2,再加上2个字节就刚好按两个字的形式排列B。

要点10、为什么继承一个没有虚析构函数的类是危险的?

  一个没有虚析构函数的类意味着不能做为一个基类。如std::string,std::complex, 和 std::vector 都是这样的。为什么继承一个没有虚析构函数的类是危险的?当你公有继承创建一个从基类继承的相关类时,指向新类对象中的指针和引用实际上都指向了起源的对象。因为析构函数不是虚函数,所以当你delete一个这样的类时,C++就不会调用析构函数链。举个例子说明:

class A
{
 public:
 ~A() // 不是虚函数
 {
 // ...
 }
};
class B: public A //错; A没有虚析构函数
{
 public:
 ~B()
 {
 // ...
 }
};

int main()
{
 A * p = new B; //看上去是对的
 delete p; //错,B的析构函没有被调用
}

要点11、以友元类声明嵌套的类

  当你以友元类声明一个嵌套的类时,把友元声明放在嵌套类声明的后面,而不前面。

class A
{
 private:
 int i;
 public:
 class B //嵌套类声明在前
 {
  public:
  B(A & a) { a.i=0;};
 };
 friend class B;//友元类声明
};

  如果你把友元类声明放在声明嵌套类的前面,编译器将抛弃友元类后的其它声明。 
 

*/

/*
浅谈内存泄漏
 
 
 
对于一个c/c++程序员来说,内存泄漏是一个常见的也是令人头疼的问题。已经有许多技术被研究出来以应对这个问题,比如Smart Pointer,Garbage Collection等。Smart Pointer技术比较成熟,STL中已经包含支持Smart Pointer的class,但是它的使用似乎并不广泛,而且它也不能解决所有的问题;Garbage Collection技术在Java中已经比较成熟,但是在c/c++领域的发展并不顺畅,虽然很早就有人思考在C++中也加入GC的支持。现实世界就是这样的,作为一个c/c++程序员,内存泄漏是你心中永远的痛。不过好在现在有许多工具能够帮助我们验证内存泄漏的存在,找出发生问题的代码。

  内存泄漏的定义

  一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。以下这段小程序演示了堆内存发生泄漏的情形:

  void MyFunction(int nSize)
  {
  char* p= new char[nSize];
  if( !GetStringFrom( p, nSize ) ){
  MessageBox(“Error”);
  return;
  }
  …//using the string pointed by p;
  delete p;
  }
例一

  当函数GetStringFrom()返回零的时候,指针p指向的内存就不会被释放。这是一种常见的发生内存泄漏的情形。程序在入口处分配内存,在出口处释放内存,但是c函数可以在任何地方退出,所以一旦有某个出口处没有释放应该释放的内存,就会发生内存泄漏。

  广义的说,内存泄漏不仅仅包含堆内存的泄漏,还包含系统资源的泄漏(resource leak),比如核心态HANDLE,GDI Object,SOCKET, Interface等,从根本上说这些由操作系统分配的对象也消耗内存,如果这些对象发生泄漏最终也会导致内存的泄漏。而且,某些对象消耗的是核心态内存,这些对象严重泄漏时会导致整个操作系统不稳定。所以相比之下,系统资源的泄漏比堆内存的泄漏更为严重。

  GDI Object的泄漏是一种常见的资源泄漏:
  void CMyView::OnPaint( CDC* pDC )
  {
  CBitmap bmp;
  CBitmap* pOldBmp;
  bmp.LoadBitmap(IDB_MYBMP);
  pOldBmp = pDC->SelectObject( &bmp );
  …
  if( Something() ){
  return;
  }
  pDC->SelectObject( pOldBmp );
  return;
  }
  当函数Something()返回非零的时候,程序在退出前没有把pOldBmp选回pDC中,这会导致pOldBmp指向的HBITMAP对象发生泄漏。这个程序如果长时间的运行,可能会导致整个系统花屏。这种问题在Win9x下比较容易暴露出来,因为Win9x的GDI堆比Win2k或NT的要小很多。

  内存泄漏的发生方式:

  以发生的方式来分类,内存泄漏可以分为4类:

  1. 常发性内存泄漏。发生内存泄漏的代码会被多次执行到,每次被执行的时候都会导致一块内存泄漏。比如例二,如果Something()函数一直返回True,那么pOldBmp指向的HBITMAP对象总是发生泄漏。

  2. 偶发性内存泄漏。发生内存泄漏的代码只有在某些特定环境或操作过程下才会发生。比如例二,如果Something()函数只有在特定环境下才返回True,那么pOldBmp指向的HBITMAP对象并不总是发生泄漏。常发性和偶发性是相对的。对于特定的环境,偶发性的也许就变成了常发性的。所以测试环境和测试方法对检测内存泄漏至关重要。

  3. 一次性内存泄漏。发生内存泄漏的代码只会被执行一次,或者由于算法上的缺陷,导致总会有一块仅且一块内存发生泄漏。比如,在类的构造函数中分配内存,在析构函数中却没有释放该内存,但是因为这个类是一个Singleton,所以内存泄漏只会发生一次。另一个例子:

  char* g_lpszFileName = NULL;

  void SetFileName( const char* lpcszFileName )

  {

  if( g_lpszFileName ){

  free( g_lpszFileName );

  }

  g_lpszFileName = strdup( lpcszFileName );
 
  }
  例三

  如果程序在结束的时候没有释放g_lpszFileName指向的字符串,那么,即使多次调用SetFileName(),总会有一块内存,而且仅有一块内存发生泄漏。

  4. 隐式内存泄漏。程序在运行过程中不停的分配内存,但是直到结束的时候才释放内存。严格的说这里并没有发生内存泄漏,因为最终程序释放了所有申请的内存。但是对于一个服务器程序,需要运行几天,几周甚至几个月,不及时释放内存也可能导致最终耗尽系统的所有内存。所以,我们称这类内存泄漏为隐式内存泄漏。举一个例子:

  class Connection
  {
  public:
  Connection( SOCKET s);
  ~Connection();
  …
  private:
  SOCKET _socket;
  …
  };

  class ConnectionManager
  {
  public:
  ConnectionManager(){
  }
  ~ConnectionManager(){
  list ::iterator it;
  for( it = _connlist.begin(); it != _connlist.end(); ++it ){
  delete (*it);
  }
  _connlist.clear();
  }
  void OnClientConnected( SOCKET s ){
  Connection* p = new Connection(s);
  _connlist.push_back(p);
  }

  void OnClientDisconnected( Connection* pconn ){
  _connlist.remove( pconn );
  delete pconn;
  }

  private:
  list _connlist;
  };

  例四

  假设在Client从Server端断开后,Server并没有呼叫OnClientDisconnected()函数,那么代表那次连接的Connection对象就不会被及时的删除(在Server程序退出的时候,所有Connection对象会在ConnectionManager的析构函数里被删除)。当不断的有连接建立、断开时隐式内存泄漏就发生了。

  从用户使用程序的角度来看,内存泄漏本身不会产生什么危害,作为一般的用户,根本感觉不到内存泄漏的存在。真正有危害的是内存泄漏的堆积,这会最终消耗尽系统所有的内存。从这个角度来说,一次性内存泄漏并没有什么危害,因为它不会堆积,而隐式内存泄漏危害性则非常大,因为较之于常发性和偶发性内存泄漏它更难被检测到。 
 

*/

/*
printf()函数参数格式详解
 
 
 
printf的格式控制的完整格式:
% - 0 m.n l或h 格式字符
下面对组成格式说明的各项加以说明:
①%:表示格式说明的起始符号,不可缺少。
②-:有-表示左对齐输出,如省略表示右对齐输出。
③0:有0表示指定空位填0,如省略表示指定空位不填。
④m.n:m指域宽,即对应的输出项在输出设备上所占的字符数。N指精度。用于说明输出的实型数的小数位数。为指定n时,隐含的精度为n=6位。
⑤l或h:l对整型指long型,对实型指double型。h用于将整型的格式字符修正为short型。

---------------------------------------
格式字符
格式字符用以指定输出项的数据类型和输出格式。
①d格式:用来输出十进制整数。有以下几种用法:
%d:按整型数据的实际长度输出。
%md:m为指定的输出字段的宽度。如果数据的位数小于m,则左端补以空格,若大于m,则按实际位数输出。
%ld:输出长整型数据。
②o格式:以无符号八进制形式输出整数。对长整型可以用"%lo"格式输出。同样也可以指定字段宽度用“%mo”格式输出。
例:
main()
{ int a = -1;
printf("%d, %o", a, a);

运行结果:-1,177777
程序解析:-1在内存单元中(以补码形式存放)为(1111111111111111)2,转换为八进制数为(177777)8。
③x格式:以无符号十六进制形式输出整数。对长整型可以用"%lx"格式输出。同样也可以指定字段宽度用"%mx"格式输出。
④u格式:以无符号十进制形式输出整数。对长整型可以用"%lu"格式输出。同样也可以指定字段宽度用“%mu”格式输出。
⑤c格式:输出一个字符。
⑥s格式:用来输出一个串。有几中用法
%s:例如:printf("%s", "CHINA")输出"CHINA"字符串(不包括双引号)。
%ms:输出的字符串占m列,如字符串本身长度大于m,则突破获m的限制,将字符串全部输出。若串长小于m,则左补空格。
%-ms:如果串长小于m,则在m列范围内,字符串向左靠,右补空格。
%m.ns:输出占m列,但只取字符串中左端n个字符。这n个字符输出在m列的右侧,左补空格。
%-m.ns:其中m、n含义同上,n个字符输出在m列范围的左侧,右补空格。如果n>m,则自动取n值,即保证n个字符正常输出。
⑦f格式:用来输出实数(包括单、双精度),以小数形式输出。有以下几种用法:
%f:不指定宽度,整数部分全部输出并输出6位小数。
%m.nf:输出共占m列,其中有n位小数,如数值宽度小于m左端补空格。
%-m.nf:输出共占n列,其中有n位小数,如数值宽度小于m右端补空格。
⑧e格式:以指数形式输出实数。可用以下形式:
%e:数字部分(又称尾数)输出6位小数,指数部分占5位或4位。
%m.ne和%-m.ne:m、n和”-”字符含义与前相同。此处n指数据的数字部分的小数位数,m表示整个输出数据所占的宽度。
⑨g格式:自动选f格式或e格式中较短的一种输出,且不输出无意义的零。

---------------------------------------
关于printf函数的进一步说明:
如果想输出字符"%",则应该在“格式控制”字符串中用连续两个%表示,如:
printf("%f%%", 1.0/3);
输出0.333333%。

---------------------------------------
对于单精度数,使用%f格式符输出时,仅前7位是有效数字,小数6位.
对于双精度数,使用%lf格式符输出时,前16位是有效数字,小数6位.
 
 
*/

/*
STL中map用法详解
 
 
 
Map是STL的一个关联容器,它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值)的数据处理能力,由于这个特性,它完成有可能在我们处理一对一数据的时候,在编程上提供快速通道。这里说下map内部数据的组织,map内部自建一颗红黑树(一种非严格意义上的平衡二叉树),这颗树具有对数据自动排序的功能,所以在map内部所有的数据都是有序的,后边我们会见识到有序的好处。

下面举例说明什么是一对一的数据映射。比如一个班级中,每个学生的学号跟他的姓名就存在着一一映射的关系,这个模型用map可能轻易描述,很明显学号用int描述,姓名用字符串描述(本篇文章中不用char *来描述字符串,而是采用STL中string来描述),下面给出map描述代码:

Map<int, string> mapStudent;

1.       map的构造函数

map共提供了6个构造函数,这块涉及到内存分配器这些东西,略过不表,在下面我们将接触到一些map的构造方法,这里要说下的就是,我们通常用如下方法构造一个map:

Map<int, string> mapStudent;

2.       数据的插入

在构造map容器后,我们就可以往里面插入数据了。这里讲三种插入数据的方法:

第一种:用insert函数插入pair数据,下面举例说明(以下代码虽然是随手写的,应该可以在VC和GCC下编译通过,大家可以运行下看什么效果,在VC下请加入这条语句,屏蔽4786警告  #pragma warning (disable:4786) )

#include <map>

#include <string>

#include <iostream>

Using namespace std;

Int main()

{

       Map<int, string> mapStudent;

       mapStudent.insert(pair<int, string>(1, “student_one”));

       mapStudent.insert(pair<int, string>(2, “student_two”));

       mapStudent.insert(pair<int, string>(3, “student_three”));

       map<int, string>::iterator  iter;

       for(iter = mapStudent.begin(); iter != mapStudent.end(); iter++)

{

       Cout<<iter->first<<”   ”<<iter->second<<end;

}

}

第二种:用insert函数插入value_type数据,下面举例说明

#include <map>

#include <string>

#include <iostream>

Using namespace std;

Int main()

{

       Map<int, string> mapStudent;

       mapStudent.insert(map<int, string>::value_type (1, “student_one”));

       mapStudent.insert(map<int, string>::value_type (2, “student_two”));

       mapStudent.insert(map<int, string>::value_type (3, “student_three”));

       map<int, string>::iterator  iter;

       for(iter = mapStudent.begin(); iter != mapStudent.end(); iter++)

{

       Cout<<iter->first<<”   ”<<iter->second<<end;

}

}

第三种:用数组方式插入数据,下面举例说明

#include <map>

#include <string>

#include <iostream>

Using namespace std;

Int main()

{

       Map<int, string> mapStudent;

       mapStudent[1] =  “student_one”;

       mapStudent[2] =  “student_two”;

       mapStudent[3] =  “student_three”;

       map<int, string>::iterator  iter;

       for(iter = mapStudent.begin(); iter != mapStudent.end(); iter++)

{

       Cout<<iter->first<<”   ”<<iter->second<<end;

}

}

以上三种用法,虽然都可以实现数据的插入,但是它们是有区别的,当然了第一种和第二种在效果上是完成一样的,用insert函数插入数据,在数据的插入上涉及到集合的唯一性这个概念,即当map中有这个关键字时,insert操作是插入数据不了的,但是用数组方式就不同了,它可以覆盖以前该关键字对应的值,用程序说明

mapStudent.insert(map<int, string>::value_type (1, “student_one”));

mapStudent.insert(map<int, string>::value_type (1, “student_two”));

上面这两条语句执行后,map中1这个关键字对应的值是“student_one”,第二条语句并没有生效,那么这就涉及到我们怎么知道insert语句是否插入成功的问题了,可以用pair来获得是否插入成功,程序如下

Pair<map<int, string>::iterator, bool> Insert_Pair;

Insert_Pair = mapStudent.insert(map<int, string>::value_type (1, “student_one”));

我们通过pair的第二个变量来知道是否插入成功,它的第一个变量返回的是一个map的迭代器,如果插入成功的话Insert_Pair.second应该是true的,否则为false。

下面给出完成代码,演示插入成功与否问题

#include <map>

#include <string>

#include <iostream>

Using namespace std;

Int main()

{

       Map<int, string> mapStudent;

Pair<map<int, string>::iterator, bool> Insert_Pair;

       Insert_Pair = mapStudent.insert(pair<int, string>(1, “student_one”));

       If(Insert_Pair.second == true)

       {

              Cout<<”Insert Successfully”<<endl;

       }

       Else

       {

              Cout<<”Insert Failure”<<endl;

       }

       Insert_Pair = mapStudent.insert(pair<int, string>(1, “student_two”));

       If(Insert_Pair.second == true)

       {

              Cout<<”Insert Successfully”<<endl;

       }

       Else

       {

              Cout<<”Insert Failure”<<endl;

       }

       map<int, string>::iterator  iter;

       for(iter = mapStudent.begin(); iter != mapStudent.end(); iter++)

{

       Cout<<iter->first<<”   ”<<iter->second<<end;

}

}

大家可以用如下程序,看下用数组插入在数据覆盖上的效果

#include <map>

#include <string>

#include <iostream>

Using namespace std;

Int main()

{

       Map<int, string> mapStudent;

       mapStudent[1] =  “student_one”;

       mapStudent[1] =  “student_two”;

       mapStudent[2] =  “student_three”;

       map<int, string>::iterator  iter;

       for(iter = mapStudent.begin(); iter != mapStudent.end(); iter++)

{

       Cout<<iter->first<<”   ”<<iter->second<<end;

}

}

3.       map的大小

在往map里面插入了数据,我们怎么知道当前已经插入了多少数据呢,可以用size函数,用法如下:

Int nSize = mapStudent.size();

4.       数据的遍历

这里也提供三种方法,对map进行遍历

第一种:应用前向迭代器,上面举例程序中到处都是了,略过不表

第二种:应用反相迭代器,下面举例说明,要体会效果,请自个动手运行程序

#include <map>

#include <string>

#include <iostream>

Using namespace std;

Int main()

{

       Map<int, string> mapStudent;

       mapStudent.insert(pair<int, string>(1, “student_one”));

       mapStudent.insert(pair<int, string>(2, “student_two”));

       mapStudent.insert(pair<int, string>(3, “student_three”));

       map<int, string>::reverse_iterator  iter;

       for(iter = mapStudent.rbegin(); iter != mapStudent.rend(); iter++)

{

       Cout<<iter->first<<”   ”<<iter->second<<end;

}

}

第三种:用数组方式,程序说明如下

#include <map>

#include <string>

#include <iostream>

Using namespace std;

Int main()

{

       Map<int, string> mapStudent;

       mapStudent.insert(pair<int, string>(1, “student_one”));

       mapStudent.insert(pair<int, string>(2, “student_two”));

       mapStudent.insert(pair<int, string>(3, “student_three”));

       int nSize = mapStudent.size()

//此处有误,应该是 for(int nIndex = 1; nIndex <= nSize; nIndex++)


//by rainfish

       for(int nIndex = 0; nIndex < nSize; nIndex++)

{

       Cout<<mapStudent[nIndex]<<end;

}

}

5.       数据的查找(包括判定这个关键字是否在map中出现)

在这里我们将体会,map在数据插入时保证有序的好处。

要判定一个数据(关键字)是否在map中出现的方法比较多,这里标题虽然是数据的查找,在这里将穿插着大量的map基本用法。

这里给出三种数据查找方法

第一种:用count函数来判定关键字是否出现,其缺点是无法定位数据出现位置,由于map的特性,一对一的映射关系,就决定了count函数的返回值只有两个,要么是0,要么是1,出现的情况,当然是返回1了

第二种:用find函数来定位数据出现位置,它返回的一个迭代器,当数据出现时,它返回数据所在位置的迭代器,如果map中没有要查找的数据,它返回的迭代器等于end函数返回的迭代器,程序说明

#include <map>

#include <string>

#include <iostream>

Using namespace std;

Int main()

{

       Map<int, string> mapStudent;

       mapStudent.insert(pair<int, string>(1, “student_one”));

       mapStudent.insert(pair<int, string>(2, “student_two”));

       mapStudent.insert(pair<int, string>(3, “student_three”));

       map<int, string>::iterator iter;

       iter = mapStudent.find(1);

if(iter != mapStudent.end())

{

       Cout<<”Find, the value is ”<<iter->second<<endl;

}

Else

{

       Cout<<”Do not Find”<<endl;

}

}

第三种:这个方法用来判定数据是否出现,是显得笨了点,但是,我打算在这里讲解

Lower_bound函数用法,这个函数用来返回要查找关键字的下界(是一个迭代器)

Upper_bound函数用法,这个函数用来返回要查找关键字的上界(是一个迭代器)

例如:map中已经插入了1,2,3,4的话,如果lower_bound(2)的话,返回的2,而upper-bound(2)的话,返回的就是3

Equal_range函数返回一个pair,pair里面第一个变量是Lower_bound返回的迭代器,pair里面第二个迭代器是Upper_bound返回的迭代器,如果这两个迭代器相等的话,则说明map中不出现这个关键字,程序说明

#include <map>

#include <string>

#include <iostream>

Using namespace std;

Int main()

{

       Map<int, string> mapStudent;

       mapStudent[1] =  “student_one”;

       mapStudent[3] =  “student_three”;

       mapStudent[5] =  “student_five”;

       map<int, string>::iterator  iter;

iter = mapStudent.lower_bound(2);

{

       //返回的是下界3的迭代器

       Cout<<iter->second<<endl;

}

iter = mapStudent.lower_bound(3);

{

       //返回的是下界3的迭代器

       Cout<<iter->second<<endl;

}

 

iter = mapStudent.upper_bound(2);

{

       //返回的是上界3的迭代器

       Cout<<iter->second<<endl;

}

iter = mapStudent.upper_bound(3);

{

       //返回的是上界5的迭代器

       Cout<<iter->second<<endl;

}

 

Pair<map<int, string>::iterator, map<int, string>::iterator> mapPair;

mapPair = mapStudent.equal_range(2);

if(mapPair.first == mapPair.second)
       {

       cout<<”Do not Find”<<endl;

}

Else

{

Cout<<”Find”<<endl;
}

mapPair = mapStudent.equal_range(3);

if(mapPair.first == mapPair.second)
       {

       cout<<”Do not Find”<<endl;

}

Else

{

Cout<<”Find”<<endl;
}

}

6.       数据的清空与判空

清空map中的数据可以用clear()函数,判定map中是否有数据可以用empty()函数,它返回true则说明是空map

7.       数据的删除

这里要用到erase函数,它有三个重载了的函数,下面在例子中详细说明它们的用法

#include <map>

#include <string>

#include <iostream>

Using namespace std;

Int main()

{

       Map<int, string> mapStudent;

       mapStudent.insert(pair<int, string>(1, “student_one”));

       mapStudent.insert(pair<int, string>(2, “student_two”));

       mapStudent.insert(pair<int, string>(3, “student_three”));

 

//如果你要演示输出效果,请选择以下的一种,你看到的效果会比较好

       //如果要删除1,用迭代器删除

       map<int, string>::iterator iter;

       iter = mapStudent.find(1);

       mapStudent.erase(iter);

 

       //如果要删除1,用关键字删除

       Int n = mapStudent.erase(1);//如果删除了会返回1,否则返回0

 

       //用迭代器,成片的删除

       //一下代码把整个map清空

       mapStudent.earse(mapStudent.begin(), mapStudent.end());

       //成片删除要注意的是,也是STL的特性,删除区间是一个前闭后开的集合

 

       //自个加上遍历代码,打印输出吧

}

8.       其他一些函数用法

这里有swap,key_comp,value_comp,get_allocator等函数,感觉到这些函数在编程用的不是很多,略过不表,有兴趣的话可以自个研究

9.       排序

这里要讲的是一点比较高深的用法了,排序问题,STL中默认是采用小于号来排序的,以上代码在排序上是不存在任何问题的,因为上面的关键字是int型,它本身支持小于号运算,在一些特殊情况,比如关键字是一个结构体,涉及到排序就会出现问题,因为它没有小于号操作,insert等函数在编译的时候过不去,下面给出两个方法解决这个问题

第一种:小于号重载,程序举例

#include <map>

#include <string>

Using namespace std;

Typedef struct tagStudentInfo

{

       Int      nID;

       String   strName;

}StudentInfo, *PStudentInfo;  //学生信息

 

Int main()

{

    int nSize;

       //用学生信息映射分数

       map<StudentInfo, int>mapStudent;

    map<StudentInfo, int>::iterator iter;

       StudentInfo studentInfo;

       studentInfo.nID = 1;

       studentInfo.strName = “student_one”;

       mapStudent.insert(pair<StudentInfo, int>(studentInfo, 90));

       studentInfo.nID = 2;

       studentInfo.strName = “student_two”;

mapStudent.insert(pair<StudentInfo, int>(studentInfo, 80));

 

for (iter=mapStudent.begin(); iter!=mapStudent.end(); iter++)

    cout<<iter->first.nID<<endl<<iter->first.strName<<endl<<iter->second<<endl;

 

}

以上程序是无法编译通过的,只要重载小于号,就OK了,如下:

Typedef struct tagStudentInfo

{

       Int      nID;

       String   strName;

       Bool operator < (tagStudentInfo const& _A) const

       {

              //这个函数指定排序策略,按nID排序,如果nID相等的话,按strName排序

              If(nID < _A.nID)  return true;

              If(nID == _A.nID) return strName.compare(_A.strName) < 0;

              Return false;

       }

}StudentInfo, *PStudentInfo;  //学生信息

第二种:仿函数的应用,这个时候结构体中没有直接的小于号重载,程序说明

#include <map>

#include <string>

Using namespace std;

Typedef struct tagStudentInfo

{

       Int      nID;

       String   strName;

}StudentInfo, *PStudentInfo;  //学生信息

 

Classs sort

{

       Public:

       Bool operator() (StudentInfo const &_A, StudentInfo const &_B) const

       {

              If(_A.nID < _B.nID) return true;

              If(_A.nID == _B.nID) return _A.strName.compare(_B.strName) < 0;

              Return false;

       }

};

 

Int main()

{

       //用学生信息映射分数

       Map<StudentInfo, int, sort>mapStudent;

       StudentInfo studentInfo;

       studentInfo.nID = 1;

       studentInfo.strName = “student_one”;

       mapStudent.insert(pair<StudentInfo, int>(studentInfo, 90));

       studentInfo.nID = 2;

       studentInfo.strName = “student_two”;

mapStudent.insert(pair<StudentInfo, int>(studentInfo, 80));

}

10.   另外

由于STL是一个统一的整体,map的很多用法都和STL中其它的东西结合在一起,比如在排序上,这里默认用的是小于号,即less<>,如果要从大到小排序呢,这里涉及到的东西很多,在此无法一一加以说明。

还要说明的是,map中由于它内部有序,由红黑树保证,因此很多函数执行的时间复杂度都是log2N的,如果用map函数可以实现的功能,而STL  Algorithm也可以完成该功能,建议用map自带函数,效率高一些。

下面说下,map在空间上的特性,否则,估计你用起来会有时候表现的比较郁闷,由于map的每个数据对应红黑树上的一个节点,这个节点在不保存你的数据时,是占用16个字节的,一个父节点指针,左右孩子指针,还有一个枚举值(标示红黑的,相当于平衡二叉树中的平衡因子),我想大家应该知道,这些地方很费内存了吧,不说了…… 
 

*/

/*
深入理解sizeof
 
 
 
最近在论坛里总有人问关于sizeof的问题,并且本人对这个问题也一直没有得到很好的解决,索性今天对它来个较为详细的总结,同时结合strlen进行比较,如果能对大家有点点帮助,这是我最大的欣慰了。

  一、好首先看看sizeof和strlen在MSDN上的定义:

  首先看一MSDN上如何对sizeof进行定义的:

sizeof Operator
sizeof expression
The sizeof keyword gives the amount of storage, in bytes, associated with a variable or a type
(including aggregate types). This keyword returns a value of type size_t.
The expression is either an identifier or a type-cast expression (a type specifier
enclosed in parentheses).
When applied to a structure type or variable, sizeof returns the actual size, which may include
padding bytes inserted for alignment. When applied to a statically dimensioned array, sizeof
returns the size of the entire array. The sizeof operator cannot return the size of dynamically
allocated arrays or external arrays.
  然后再看一下对strlen是如何定义的:

strlen
Get the length of a string.
Routine Required Header:
strlen <string.h>
size_t strlen( const char *string );
Parameter
string:Null-terminated string
Libraries
All versions of the C run-time libraries.
Return Value
Each of these functions returns the number of characters in string, excluding the terminal
NULL. No return value is reserved to indicate an error.
Remarks
Each of these functions returns the number of characters in string, not including the
terminating null character. wcslen is a wide-character version of strlen; the argument of
wcslen is a wide-character string. wcslen and strlen behave identically otherwise.

  二、由几个例子说开去。

  第一个例子:

char* ss = "0123456789";
sizeof(ss) 结果 4 ===》ss是指向字符串常量的字符指针
sizeof(*ss) 结果 1 ===》*ss是第一个字符
char ss[] = "0123456789";
sizeof(ss) 结果 11 ===》ss是数组,计算到0位置,因此是10+1
sizeof(*ss) 结果 1 ===》*ss是第一个字符
char ss[100] = "0123456789";
sizeof(ss) 结果是100 ===》ss表示在内存中的大小 100×1
strlen(ss) 结果是10 ===》strlen是个函数内部实现是用一个循环计算到0为止之前
int ss[100] = "0123456789";
sizeof(ss) 结果 400 ===》ss表示再内存中的大小 100×4
strlen(ss) 错误 ===》strlen的参数只能是char* 且必须是以///'///'结尾的
char q[]="abc";
char p[]="an";
sizeof(q),sizeof(p),strlen(q),strlen(p);
结果是 4 3 3 2
  第二个例子:

class X
{
int i;
int j;
char k;
};
X x;
cout<<sizeof(X)<<endl; 结果 12 ===》内存补齐
cout<<sizeof(x)<<endl; 结果 12 同上
  第三个例子:

char szPath[MAX_PATH]
  如果在函数内这样定义,那么sizeof(szPath)将会是MAX_PATH,但是将szPath作为虚参声明时(void fun(char szPath[MAX_PATH])),sizeof(szPath)却会是4(指针大小)

  三、sizeof深入理解。

  1.sizeof操作符的结果类型是size_t,它在头文件中typedef为unsigned int类型。该类型保证能容纳实现所建立的最大对象的字节大小。
  2.sizeof是算符,strlen是函数。
  3.sizeof可以用类型做参数,strlen只能用char*做参数,且必须是以///'///'0///'///'结尾的。sizeof还可以用函数做参数,比如:
short f();
printf("%dn", sizeof(f()));
输出的结果是sizeof(short),即2。
  4.数组做sizeof的参数不退化,传递给strlen就退化为指针了。
  5.大部分编译程序 在编译的时候就把sizeof计算过了 是类型或是变量的长度这就是sizeof(x)可以用来定义数组维数的原因
char str[20]="0123456789";
int a=strlen(str); //a=10;
int b=sizeof(str); //而b=20;
  6.strlen的结果要在运行的时候才能计算出来,时用来计算字符串的长度,不是类型占内存的大小。
  7.sizeof后如果是类型必须加括弧,如果是变量名可以不加括弧。这是因为sizeof是个操作符不是个函数。
  8.当适用了于一个结构类型时或变量, sizeof 返回实际的大小, 当适用一静态地空间数组, sizeof 归还全部数组的尺 寸。 sizeof 操作符不能返回动态地被分派了的数组或外部的数组的尺寸
  9.数组作为参数传给函数时传的是指针而不是数组,传递的是数组的首地址,如:
fun(char [8])
fun(char [])
都等价于 fun(char *) 在C++里传递数组永远都是传递指向数组首元素的指针,编译器不知道数组的大小如果想在函数内知道数组的大小, 需要这样做:进入函数后用memcpy拷贝出来,长度由另一个形参传进去
fun(unsiged char *p1, int len)
{
unsigned char* buf = new unsigned char[len+1]
memcpy(buf, p1, len);
}
  有关内容见: C++ PRIMER?
  10.计算结构变量的大小就必须讨论数据对齐问题。为了CPU存取的速度最快(这同CPU取数操作有关,详细的介绍可以参考一些计算机原理方面的书),C++在处理数据时经常把结构变量中的成员的大小按照4或8的倍数计算,这就叫数据对齐(data alignment)。这样做可能会浪费一些内存,但理论上速度快了。当然这样的设置会在读写一些别的应用程序生成的数据文件或交换数据时带来不便。MS VC++中的对齐设定,有时候sizeof得到的与实际不等。一般在VC++中加上#pragma pack(n)的设定即可.或者如果要按字节存储,而不进行数据对齐,可以在Options对话框中修改Advanced compiler页中的Data alignment为按字节对齐。
11.sizeof操作符不能用于函数类型,不完全类型或位字段。不完全类型指具有未知存储大小的数据类型,如未知存储大小的数组类型、未知内容的结构或联合类型、void类型等。如sizeof(max)若此时变量max定义为int max(),sizeof(char_v) 若此时char_v定义为char char_v [MAX]且MAX未知,sizeof(void)都不是正确形式
  四、结束语

  sizeof使用场合。

  1.sizeof操作符的一个主要用途是与存储分配和I/O系统那样的例程进行通信。例如: 
  void *malloc(size_t size), 
  size_t fread(void * ptr,size_t size,size_t nmemb,FILE * stream)。
  2.用它可以看看一类型的对象在内存中所占的单元字节。
void * memset(void * s,int c,sizeof(s))
  3.在动态分配一对象时,可以让系统知道要分配多少内存。
  4.便于一些类型的扩充,在windows中就有很多结构内型就有一个专用的字段是用来放该类型的字节大小。
  5.由于操作数的字节数在实现时可能出现变化,建议在涉及到操作数字节大小时用sizeof来代替常量计算。
  6.如果操作数是函数中的数组形参或函数类型的形参,sizeof给出其指针的大小。
*/

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值