C语言总结

原创 2018年04月16日 09:46:30

C 语言易错点知识总结

关键字

  • 变量声明与定义
    定义声明最重要的区别:
     <1 定义创建了对象并为这个计算对象所占内存空间大小对象分配了内存,声明没有分配内存。
     <2 定义只能一次,声明可以多次。
     <3 声明的2个作用:
         a.告诉编译器,这个名字已经匹配到一块内存上,下面的代码用到变量或对象是在别的地方定义的。
         b.告诉编译器,我这个名字我先预定了,别的地方再也不能用它来作为变量名或对象名。
    
    eg:
    函数声明 void fun(int i, char c); 变量声明 extern int i;
    函数定义 void fun(int i, char c){} 变量定义 int i = 0  ;
    
  • 命名规则
    <1 对在多个文件之间共同使用的全局变量或函数要加范围限定符
    <2 所有宏定义、枚举常数、只读变量全用大写字母命名,用下划线分割单词。
    <3 定义变量的同时千万千万别忘了初始化。定义变量时编译器并不一定清空了这块内存,它的值可能是无效的数据。

  • static 关键字
     <1 第一个作用:修饰变量。变量又分为局部和全局变量,但它们都存在内存的静态区。
    静态全局变量,作用域仅限于变量被定义的文件中,其他文件即使用 extern 声明也没法
    使用他。准确地说作用域是从定义之处开始,到文件结尾处结束,在定义之处前面的那些
    代码行也不能使用它。
     静态局部变量,在函数体里面定义的,就只能在这个函数里用了,同一个文档中的其他
    函数也用不了。由于被 static 修饰的变量总是存在内存的静态区,所以即使这个函数运行结
    束,这个静态变量的值还是不会被销毁,函数下次使用时仍然能用到这个值。
     <2 第二个作用:修饰函数。函数前加 static 使得函数成为静态函数。但此处“static”的含义
    不是指存储方式,而是指对函数的作用域仅局限于本文件(所以又称内部函数)。使用内部函
    数的好处是:不同的人编写不同的函数时,不用担心自己定义的函数,是否会与其它文件
    中的函数同名。
     static 在 C 中有了二种含义:

      1.static 是为了表示退出一个块后仍然存在的局部变量。
      2.用来表示不能被其它文件访问的全局变量和函数。
    

    代码1:

    #include< stdio.h>
    static int j;
    void fun1(void)
    {
      static int i = 0;
      i++;
    }
    void fun2(void)
    {
      j = 0;
      j++;
    }
    int main()
    {
      for (int k = 0; k < 10; k++)
      {
          fun1();
          fun2();
      }
      return 0;
    }

解析: i=10, j=1

  1. i 声明在函数内部,且有static,所以它是属于局部静态变量。
    静态变量的特点是:第一次调用会初始化,且会一直保留最后的值,后面每次调用的时候不再重新初始化。所以第一次进来执行 i=0 和 i++,以后每次进来都只执行 i++。但是其他函数不能访问与使用,本函数下次使用仍然能使用该值。
  2. j 属于全局静态变量,文件内皆可以使用,每次进入到 fun 函数,皆执行 j=0; j++两句,所以值最终仍为 1

代码2:

#include< stdio.h>
static int a = 1;
void fun1(void)
{
    a = 2;
}
void fun2(void)
{
    int a = 3;
}
void fun3(void)
{
    static int a = 4;
}
int main()
{
    printf("%d", a);
    fun1();
    printf("%d", a);
    fun2();
    printf("%d", a);
    fun3();
    printf("%d", a);
}

解析: 打印 1222
首先声明了一个静态全局变量i,首次输出肯定是 1。
第二次输出,访问到了早已定义的全局变量i,并改写值,第二次为 2。
第三次输出,内部定义了一个同名的变量, 它并没有返回值或者被该次执行程序以外任何程序读取a值,所以第三次输出仍然是2。
第四次输出,定义了一个静态的局部变量,静态局部变量在函数调用结束后仍然存在,及它的内存空间不会被释放,但其他函数是不能引用它的,所以,两个静态变量虽然同名,但是并不是一样的东西,输出为2。

  • sizeof
    sizeof 为关键字,在计算变量所占空间大小时,括号可以省略,而计算类型(模子)大小时不能省略。
    eg:1.int a[100]; sizeof (a) 的值是多少? sizeof(a[100])呢? //请尤其注意本例。 sizeof(&a)呢? sizeof(&a[0])呢?
      int a[100]; //声明了一个有100个int类型元素的数组, 数组下标从0~99, 所以a是数组名, 代表数组的首地址, 也就是&a[0]
      sizeof (a) //返回数组a在内存中所占的空间大小,以字节为单位, 也就是sizeof(a) = sizeof(int) * 100 = 4 * 100 = 400bytes
                //a本身是个地址,用int存放,占用4个字节。int就是4,a[100]一共是100个int变量,就是400.
      sizeof(a[100])//求第100个元素的大小 ,用int存放,占用4个字节
      sizeof(&a)//表示存放a的地址的空间内存的地址的大小,即&a也是个地址值。一个地址用int存放也是4个字节
      sizeof(&a[0])//取第一个元素的地址,用int存放,占用4个字节
    
  1. int b[100];
     void fun(int b[100])
     {
      sizeof(b); //等于4
     }
     void fun(int* b)
     {
       sizeof(b); //等于4
     }

    两个是等价的。
    当你在调用fun函数时,他们内部实际上是这样做的,先将b[100]数组的首个元素的地址赋值给了函数参数列表中的那个b指针,虽然都是b,但是却是在不同额作用域,故你可以理解成fun参数列表中的b是一个指针,在32位系统中,指针永远是4个字节。

  • signed 和 unsigned关键字
    表示范围:一个 32 位的 signed int 类型整数其值表示法范围为: - 2^31~ (2^31)-1; 8 位的char 类型数其值表示的范围为- 2^7 ~ (2^7) -1。
     一个 32 位的 unsigned int 类型整数其值表示法范围为: 0~ (2^32) -1; 8 位的 char 类型数其值表示的范围为 0~ (2^8) -1
    #include< stdio.h>
    #include< string.h>
    int main()
    {
      char a[1000];
      int i;
      for (i = 0; i<1000; i++)
      {
          a[i] = -1 - i;
      }
      printf("%d", strlen(a));
      return 0;
    }

答案是 255
分析:按照负数补码的规则,可以知道-1 的补码为 0xff, -2 的补码为 0xfe……当 i 的值为 127
时, a[127]的值为-128,而-128 是 char 类型数据能表示的最小的负数。当 i 继续增加, a[128]
的值肯定不能是-129。因为这时候发生了溢出, -129 需要 9 位才能存储下来,而 char 类型
数据只有 8 位,所以最高位被丢弃。剩下的 8 位是原来 9 位补码的低 8 位的值,即 0x7f。
当 i 继续增加到 255 的时候, -256 的补码的低 8 位为 0。然后当 i 增加到 256 时, -257 的补码的低 8 位全为 1,即低八位的补码为 0xff,如此又开始一轮新的循环……
 按照上面的分析, a[0]到 a[254]里面的值都不为 0,而 a[255]的值为 0。 strlen 函数是计
算字符串长度的,并不包含字符串最后的‘ \0’。而判断一个字符串是否结束的标志就是看
是否遇到‘ \0’。如果遇到‘ \0’,则认为本字符串结束。

  • bool、float、int、指针与"0"值比较
  1. if(bTestFlag); if(!bTestFlag);
    bool bTestFlag = FALSE; 为了安全考虑,bool 值初始化false比较好,如果调用发生异常,不会被误调用
  2. if((fTestVal >= -EPSINON) && (fTestVal <= EPSINON)); //EPSINON 为定义好的精度。
  3. if(i == 0);
  4. if(NULL == p); if(NULL != p);
  • 堆和栈
    栈:保存局部变量。栈上的内容只在函数的范围内存在,当函数运行结束,这些内容会自动被销毁。其特点是效率高,但空间大小有限。栈是向由高地址低地址扩展的数据结构。
    堆:由 malloc 系列函数或 new 操作符分配的内存。其生命周期由 free 或 delete 决定。
    没有释放之前一直存在,直到程序结束。其特点是使用灵活,空间比较大,但而且容易产生内存碎片,容易出错。堆是由低地址向高地址扩展的数据结构

  • if 语句
    1.先处理正常情况,再处理异常情况。
    在编写代码是,要使得正常情况的执行代码清晰,确认那些不常发生的异常情况处理代码不会遮掩正常的执行路径。这样对于代码的可读性和性能都很重要。
    2.if 语句后不能写分号,走则默认执行语句。
    3.if 语句代码块应用大括号{}包含在内,否则默认只执行第一句,容易出错。

  • switch-case关键字
    1.每个 case 语句的结尾绝对不要忘了加 break,否则将导致多个分支重叠(除非
    有意使多个分支重叠)。
    2.最后必须使用 default 分支。即使程序真的不需要 default 处理,也应该保留
    语句:default :
          break;
    这样做并非画蛇添足,可以避免让人误以为你忘了 default 处理。
    3.case 后面只能是整型或字符型的常量或常量表达式。
    4.case 语句排列顺序:按字母或数字顺序排列各条 case 语句;把正常情况放在前面,而把异常情况放在后面;按执行频率排列

  • do、while、for 关键字
     while 循环:先判断 while 后面括号里的值,如果为真则执行其后面的代码;否则不执行。 while( 1)表示死循环。
     do-while 循环:先执行 do 后面的代码,然后再判断 while 后面括号里的值,如果为真,循环开始;否则,循环不开始。其用法与 while 循环没有区别,但相对较少用。
     for 循环: for 循环可以很容易的控制循环次数,多用于事先知道循环次数的情况下。
    tips:

      1.将最长的循环放在最内层,最短的循环放在最外层,以减少 CPU 跨切循环层的次数。长循环在最内层,效率高。
      2.建议 for 语句的循环控制变量的取值采用“半开半闭区间”写法。
      3.不能在 for 循环体内修改循环变量,防止循环失控。
      4.循环要尽可能的短,要使代码清晰,一目了然。采取重新设计循环或者封装到子函数
      5.把循环嵌套控制在 3 层以内。
    
  • break 与 continue关键字
     break 关键字很重要,表示终止本层循环。
     continue 表示终止本次(本轮) 循环。当代码执行到 continue 时,本轮循环终止,进入下一轮循环

  • 禁用goto 关键字

  • void 关键字

    1.任何类型的指针都可以直接赋值给void *,无需进行强制类型转换;但这并不意味着, void *也可以无需强制类型转换地赋给其它类型的指针。
      void *p1;
      int *p2;
      p1 = p2; //正确
      p1 = p2; //错误 提示“'=' : cannot convert from 'void *' to 'int *'”
    2.如果函数没有返回值,那么应声明为 void 类型
      在 C 语言中,凡不加返回值类型限定的函数,就会被编译器作为返回整型值处理
    3.如果函数无参数,那么应声明其参数为 void
    
  • return 关键字
     return 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结束时被自动销毁。

  • const 关键字
    1.定义 const 只读变量,具有不可变性。
    2.const 修饰的只读变量必须在定义的同时初始化,case 语句后面不可以是 const 修饰的只读变量。
    3.const修饰指针

    const int p; // p 可变, p 不可变,即指针可以指向别的内存,指针的值不可改变
    int const p; // p 可变, p 不可变,即指针可以指向别的内存,指针的值不可改变
    int const p; // p 不可变,p 可变,即指针不可以指向别的内存,指针的值可以改变
    const int *const p; //指针 p 和 *p 不可变,即指针和指针的值都不可以改变
    先忽略类型名(编译器解析的时候也是忽略类型名),我们看 const 离哪个近。“近水楼
    台先得月”,离谁近就修饰谁。
    4.const 修饰符也可以修饰函数的参数,当不希望这个参数值被函数体内意外改变时使
    用。
    5.const 修饰符也可以修饰函数的返回值,返回值不可被改变。

  • volatile 关键字
    到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问
    如果变量 i 是一个寄存器变量或者表示一个端口数据或者是多个线程的共享数据,就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。

  • extern 关键字
    extern 可以置于变量或者函数前,以标示变量或者函数的定义在别的文件中,下面的代码用到的这些变量或函数是外来的,不是本文件定义的,提示编译器遇到此变量和函数时在其他模块中寻找其定义。

  • struct 关键字
    结构体所占的内存大小是其成员所占内存之和,空结构体的大小就定为 1 个 byte

    typedef structst_type
    {
      int i;
      int a[];
    }type_a;
    type_a *p = (type_a*)malloc(sizeof(type_a)+100*sizeof(int));

为结构体指针 p 分配了一块内存。用 p->a[n]就能简单地访问可变长元素。

  • union 关键字
    union 维护足够的空间来置放多个数据成员中的“一种”,而不是为每一个数据成员配置空间,
    在 union 中所有的数据成员共用一个空间,同一时间只能储存其中一个数据成员,所有的数据成员具有相同的起始地址。
    union 主要用来压缩空间。如果一些数据不可能在同一时间同时被用到,则可以使用 union。
    应用:大小端问题
    1.定义
    大端模式( Big_endian):字数据的字数据的低字节则存放在高地址中,而高字节存储在低地址中。
    小端模式( Little_endian):字数据的字数据的低字节则存放在低地址中,而高字节存储在高地址中。
    2.程序判断当前系统的存储模式
    变量 i 占 4 个字节,但只有一个字节的值为 1,另外三个字节的值都为 0。如果取出低
    地址上的值为 0,毫无疑问,这是大端模式;如果取出低地址上的值为 1,毫无疑问,这是
    小端模式。既然如此,我们完全可以利用 union 类型数据的特点:所有成员的起始地址一致。
    参考答案如下:

    int checkSystem( )
    {
      union check
      {
          int i;
          char ch;
      } c;
      c.i = 1;
      return (c.ch ==1);
    }
  • enum 关键字
    1.例子

    enum Color
    {
      GREEN = 1,
      RED, //2
      BLUE, //3
      GREEN_RED = 10,
      GREEN_BLUE //11
    }ColorVal;
    Color 是自定义的一种数据数据类型名,而 ColorVal 为Color 类型的一个变量,也就是我们平时常说的枚举变量。
    2.下面再看看枚举与#define 宏的区别:
    1), #define 宏常量是在预编译阶段进行简单替换。枚举常量则是在编译的时候确定其值。
    2),一般在编译器里,可以调试枚举常量,但是不能调试宏常量。
    3),枚举可以一次定义大量相关的常量,而#define 宏一次只能定义一个。
    3.A),枚举能做到事, #define 宏能不能都做到?如果能,那为什么还需要枚举?
     B), sizeof( ColorVal)的值为多少?为什么?
    解析:a.枚举可以自增1,这样不用每一个值都定义,而宏必须每个值都定义。而且枚举是一个集合,代表一类值,像你代码中的颜色归为一类,方便使用,而宏不能形成集合。
       b.sizeof(ColorVal)是4,因为ColorVal是一个枚举变量,而枚举变量代表一个整数(如ColorVal = RED),而整数是4个字节。

  • typedef 关键字
    1.定义:typedef 的真正意思是给一个已经存在的数据类型(注意:是类型不是变量)取一个别
    名,而非定义一个新的数据类型。
    2.例子

    typedef struct student
    {
      //code
    }Stu_st,*Stu_pst;
    A) struct student stu1;和 Stu_st stu1;没有区别。
    B) struct student *stu2;和 Stu_pst stu2;和 Stu_st *stu2;没有区别

    3.与 const 连用
    const Stu_pst stu3;=》指针所指对象的值不能修改。
    Stu_pst const stu4;=》指针变量本身不允许被修改。
    4.typedef 与 define
    C)#define INT32 int
    unsigned INT32 i = 10;
    D)typedef int int32;
    unsigned int32 j = 10;
    其中 D)编译出错,为什么呢? C)不会出错,这很好理解,因为在预编译的时候 INT32
    被替换为 int,而 unsigned int i = 10;语句是正确的。但是,很可惜,用 typedef 取的别名不支持这种类型扩展。
    typedef static int int32 不可以
    typedef 用来定义一个变量类型的别名。static 不是变量类型。它定义存放方式。

符号

  • 注释
    1.注释不可以嵌套,因为/总是与离它最近的/匹配。
    2.编译器预处理时会将注释剔除,用空格代替原来的注释。
    3.只要斜杠( /)和星号( *)之间没有空格,都会被当作注释的开始。
    4.注释的位置应与被描述的代码相邻,可以与语句在同一行,也可以在上行,但不可放在下方。

  • 接续符和转义符
    1.C 语言里以反斜杠( \)表示断行。编译器会将反斜杠剔除掉,跟在反斜杠后面的字符
    动接续到前一行。但是注意:反斜杠之后不能有空格,反斜杠的下一行之前也不能有空格。
    2.反斜杠除了可以被用作接续符,还能被用作转义字符的开始标识

  • 单引号、双引号
    双引号引起来的都是字符串常量,单引号引起来的都是字符常量。
    eg:1,‘ 1‘,“ 1”。
    第一个是整形常数, 32 位系统下占 4 个 byte;
    第二个是字符常量,占 1 个 byte;
    第三个是字符串常量,占 2 个 byte。

  • 逻辑运算符
    短路原则

    int i=0;
    int j=0;
    if((++i>0)||(++j>0))
    {
    //打印出 i 和 j 的值。
    }
    结果:i=1;j=0。
  • 位运算符
    1.包括:& 按位与 | 按位或 ^ 按位异或 ~ 取反 << 左移 >> 右移
    2.左移和右移
    左移运算符“<<”是双目运算符。其功能把“<< ”左边的运算数的各二进位全部左移若干
    位,由“<<”右边的数指定移动的位数,高位丢弃,低位补 0。
    移运算符“>>”是双目运算符。其功能是把“>> ”左边的运算数的各二进位全部右移若
    干位, “>>”右边的数指定移动的位数。但注意:对于有符号数,在右移时,符号位将随同
    移动。当为正数时, 最高位补 0;而为负数时,符号位为 1,最高位是补 0 或是补 1 取决
    于编译系统的规定。 Turbo C 和很多系统规定为补 1
    3.应用:实现不用第三个临时变量交换两个变量的值:
     1.加减操作 a=a+b; b=a-b; a=a-b;
     2.按位异或操作 a ^= b; b ^= a;a ^= b

  • 运算符优先级 P60
    简单:单目运算符 > 算术运算符 > 关系运算符 > 位运算符 > 逻辑运算符 > 赋值运算符
    单目运算符、赋值运算符和三目运算符从右到左,双目运算符从左到右

  • i++,++i 运算符
    eg:
    1.int i = 3;
    ( ++i) +( ++i) +( ++i);
    解析:有点编译器计算出来为 18,因为 i 经过 3 次自加后变为 6,然后 3 个 6 相加得 18;
    而有的编译器计算出来为 16(比如 Visual C++6.0),先计算前两个 i 的和,这时候 i 自加两次, 2 个 i 的和为 10,然后再加上第三次自加的 i 得 16。
    2.
    A) j =(i++,i++,i++);
    B) for( i=0;i<10;i++) {  //code } C)  k = ( i++) + ( i++) + ( i++); 解析:a 结果为 2例子为逗号表达式,i 在遇到每个逗号后,认为本计算单位已经结束,i 这时候自加,逗号运算符以最后一个为所要赋的值 b 结果为 10 c 结果为 0 遇到分号才认为要++,应理解为三个i相加 3.
    for( i=0, printf( “ First=%d”, i) ;
    i<10, printf( “ Second=%d”, i) ;
    i++, printf( “ Third=%d”, i))
    {
      printf( “ Fourth=%d”, i);
    }

    解析:结果无限死循环,循环条件i<10,逗号表达式以最后一个为返回值,printf的返回值为输出字符的个数,是一个整型值。去掉second语句,则正常输出。
    4.++i+++i+++i 和 a+++++b 的括号问题
    解析: 贪心法。
    C 语言有这样一个规则:每一个符号应该包含尽可能多的字符。也就是说,编译器将程
    序分解成符号的方法是,从左到右一个一个字符地读入,如果该字符可能组成一个符号,
    那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组
    成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串
    已不再可能组成一个有意义的符号。

预处理

  • define 宏定义

      tips: 
      1.反斜杠作为接续符时,在本行其后面不能再有任何字符,空格都不行。
      2.定义宏的时候一定要注意什么时候该用空格。
      3.用 define 宏定义表达式不要吝啬括号,因为表达式是直接替换。
      4.用宏开始或结束一段注释是不行的,因为注释先于预处理指令被处理。
    
  • 文件包含

    文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编
    译,结果将生成一个目标文件。C语言提供 #include 命令来实现文件包含的操作,它实际是
    宏替换的延伸,有两种格式:
    格式 1:
      #include 
    其中, filename 为要包含的文件名称,用尖括号括起来,也称为头文件,表示预处理到
    系统规定的路径中去获得这个文件(即 C 编译系统所提供的并存放在指定的子目录下的头
    文件)。找到文件后,用文件内容替换该语句。
    格式 2:
      #include “filename”
    其中, filename 为要包含的文件名称。双引号表示预处理应在当前目录中查找文件名为
    filename 的文件,若没有找到,则按系统指定的路径信息,搜索其他目录。找到文件后,用
    文件内容替换该语句。
    需要强调的一点是: #include 是将已存在文件的内容嵌入到当前文件中。
    另外关于#include 的路径也有点要说明: include 支持相对路径,格式如 trackant(蚁迹寻
    踪)所写:
    .代表当前目录, ..代表上层目录
  • #pragma once
    只要在头文件的最开始加入这条指令就能够保证头文件被编译一次。

  • #error 
    预处理指令的作用是,编译程序时,只要遇到 #error 就会生成一个编译错误提示消息,并停止编译。

  • #line 预处理
    作用是改变当前行数和文件名称,它们是在编译程序中预先定义的标识符。
    通过这条指令,可以保证文件名是固定的,不会被这些中间文件代替,有利于进行分析

  • #pragma pack & 内存对齐
    为什么要内存对齐?
    原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;然而,对齐的内存访问仅需要一次访问。(字,双字,和四字在自然边界上不需要在内存中对齐。)

#pragma pack()来改变编译器的默认对齐方式
使用指令#pragma pack (n),编译器将按照 n 个字节对齐。
使用指令#pragma pack (),编译器将取消自定义字节对齐方式.

每个成员按自己的方式对齐.也就是说虽然指定了按 n 字节对齐,但并不是所有的成员都是以 n 字节对齐。
其对齐的规则是,每个成员按其类型的对齐参数(通常是这个类型的大小)和指定对齐参数(这里是 n 字节)中较小的一个对齐。即:min( n, sizeof( item )) 。并且结构的长度必须为所用过的所有对齐参数的整数倍,不够就补空字节。

首先,每个成员分别按自己的方式对齐,并能最小化长度。
其次,复杂类型(如结构)的默认对齐方式是它最长的成员的对齐方式,这样在成员是复杂类型时,可以最小化长度。
然后,对齐后的长度必须是成员中最大的对齐参数的整数倍,这样在处理数组时可以保证每一项都边界对齐。
  • # 预算符

    #define SQR(x) printf("The square of x is %d.\n", ((x)*(x)));
    如果这样使用宏:
    SQR(8);
    则输出为:
    The square of x is 64.
      引号中的字符 x 被当作普通文本来处理,而不是被当作一个可以被替换的语言符号
      可以使用“ #”,它可以把语言符号转化为字符串。
    #define SQR(x) printf("The square of "#x" is %d.\n", ((x)*(x)));
    再使用:
    SQR(8);
    则输出的是:
    The square of 8 is 64
  • ## 预算符

      #define XNAME(n) x ## n
      如果这样使用宏:
      XNAME(8)
      则会被展开成这样:
      x8
    

    就是个粘合剂,将前后两部分粘合起来。

指针和数组

 指针就是指针,指针变量在 32 位系统下,永远占 4 个 byte,其值为某一个内存的地址。指针可以指向任何地方,但是不是任何地方你都能通过这个指针变量访问到。
 数组就是数组,其大小与元素的类型和个数有关。定义数组时必须指定其元素的类型和个数。数组可以存任何类型的数据,但不能存函数。

  • 指针
    在 32 位系统下,不管什么样的指针类型,其大小都为 4byte。

    int *p = NULL;
    这句代码的意思是:定义一个指针变量 p,其指向的内存里面保存的是 int 类型的数据;在定义变量 p 的同时把 p 的值设置为0x00000000,而不是把*p 的值设置为 0x00000000。这个过程叫做初始化,是在编译的时候进行的。
    int *p;
    *p = NULL;
    同样,我们可以在编译器上调试这两行代码。第一行代码,定义了一个指针变量 p,其指向
    的内存里面保存的是 int 类型的数据;但是这时候变量 p 本身的值是多少不得而知,也就是
    说现在变量 p 保存的有可能是一个非法的地址。第二行代码,给*p 赋值为 NULL,即给 p
    指向的内存赋值为 NULL;但是由于 p 指向的内存可能是非法的,所以调试的时候编译器可
    能会报告一个内存访问错误。
    
    tips:NULL(NUL or null) == 数字0 == "\0" == " " ASCII 码值为 0  "0"ASCII码为48  
  • 将数值存储到指定的内存地址

    int *p = (int *)0x12ff7c;
    *p = 0x100;
       ==
    *(int *)0x12ff7c = 0x100;
  • 数组

  • 数组名 a 作为左值和右值的区别
    a 作为右值时其意义与&a[0]是一样,代表的是数组首元素的首地址,而不是数组的首地址。
    a 不能作为左值,我们只能访问数组的某个元素而无法把数组当一个总体进行访问。

  • a 和&a 的区别

    main()
    {
      int a[5]={1,2,3,4,5};
      int ptr=(int )(&a+1);
      printf("%d,%d",(a+1),(ptr-1));
    }打印出来的值为多少呢? 这里主要是考查关于指针加减操作的理解。

    对指针进行加 1 操作,得到的是下一个元素的地址,而不是原有地址值直接加 1。所以,一个类型为 T 的指针的移动,以 sizeof(T) 为移动单位。 因此,对上题来说, a 是一个一维数组,数组中有 5 个元素; ptr 是一个 int 型的指针。

    &a + 1: 取数组 a 的首地址,该地址的值加上 sizeof(a) 的值,即 &a + 5sizeof(int),也 就是下一个数组的首地址,显然当前指针已经越过了数组的界限。 (int )(&a+1): 则是把上一步计算出来的地址,强制转换为 int * 类型,赋值给 ptr。

    *(a+1): a,&a 的值是一样的,但意思不一样, a 是数组首元素的首地址,也就是 a[0]的 首地址, &a 是数组的首地址, a+1 是数组下一元素的首地址,即 a[1]的首地址,&a+1 是下一 个数组的首地址。所以输出 2

    (ptr-1): 因为 ptr 是指向 a[5],并且 ptr 是 int 类型,所以 *(ptr-1) 是指向 a[4] , 输出 5。

    tips:指针变量与一个整数相加减并不是用指针变量里的地址直接加减这个整数。

  • 指针和数组的定义与声明
    文件 1 中定义的数组在文件 2 中声明为指针会发生错误。
    文件 1 中定义为指针,而在文件中声明为数组也会发生错误:

  • 指针和数组的对比

    指针 
    1.保存数据的地址,任何存入指针变量 p 的数据都会被当作地址来处理。 p 本身的地址由
    编译器另外存储,存储在哪里,我们并不知道。
    2.间接访问数据,首先取得指针变量 p 的内容,把它作为地址,然后从这个地址提取数据或向这个地址写入数据。指针可以以指针的形式访问*(p+i);
    也可以以下标的形式访问 p[i]。但其本质都是先取 p 的内容然后加上i*sizeof(类型)个 byte 作为数据的真正地址。
    3.通常用于动态数据结构
    4.相关的函数为 malloc 和 free。 
    5.通常指向匿名数据(当然也可指向具名数据) 
    数组
    1.保存数据,数组名 a 代表的是数组首元素的首地址而不是数组的首地址。 &a 才是整个数组的首地址。 a 本身的地址由编译器另外存储,存储在哪里,我们并不知道。
    2.直接访问数据,数组名 a 是整个数组的名字,数组内每个元素并没有名字。只能通过“具名+匿名”的方式来访问其某个元素,不能把数组当一个整体来进行读写操作。
    数组可以以指针的形式访问*(a+i); 也可以以下标的形式访问 a[i]。但其本质都是 a 所代表的数组首元素的首地址加上 i*sizeof(类型)个 byte 作为数据的真正地址。
    3.通常用于存储固定数目且数据类型相同的元素。
    4.隐式分配和删除
    5.自身即为数组名
  • 指针数组和数组指针

    1.指针数组:首先它是一个数组,数组的元素都是指针,数组占多少个字节由数组本身
    决定。它是“储存指针的数组”的简称。 eg: int *p1[10];
    2.数组指针:首先它是一个指针,它指向一个数组。在 32 位系统下永远是占 4 个字节,
    至于它指向的数组占多少字节,不知道。它是“指向数组的指针”的简称。eg:int (*p2)[10];


    tips: C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析成一个指向其首元
    素首地址的指针。
       函数本身是没有类型的,只有函数的返回值才有类型。

  • 无法把指针变量本身传递给一个函数

    void GetMemory( char * p, int num)
    {
      p = (char *)malloc(num*sizeof(char));
    }
    int main()
    {
      char *str = NULL;
      GetMemory( str, 10) ;
      strcpy(str,”hello”);
      free( str); //free 并没有起作用,内存泄漏
    return 0;
    }
    在运行 strcpy(str,”hello”)语句的时候发生错误。这时候观察 str 的值, 发现仍然为 NULL。
    也就是说 str 本身并没有改变,我们 malloc 的内存的地址并没有赋给 str,而是赋给了_str。
    而这个_str 是编译器自动分配和回收的,我们根本就无法使用。所以想这样获取一块内存是
    不行的。那怎么办? 两个办法:
    第一:用 return。
    char * GetMemory( char * p, int num)
    {
      p = (char *)malloc(num*sizeof(char));
      return p;
    }
    int main()
    {
      char *str = NULL;
      str = GetMemory( str, 10) ;
      strcpy(str,”hello”);
      free( str);
      return 0;
    }
    这个方法简单,容易理解。
    第二:用二级指针。
    void GetMemory( char ** p, int num)
    {
      *p = (char *)malloc(num*sizeof(char));
      return p;
    }
    int main()
    {
      char *str = NULL;
      GetMemory( &str, 10) ;
      strcpy(str,”hello”);
      free( str);
      return 0;
    }
    注意,这里的参数是&str 而非 str。这样的话传递过去的是 str 的地址,是一个值。在函
    数内部,用钥匙( “ *”)来开锁: *(&str),其值就是 str。所以 malloc 分配的内存地址是真正
    赋值给了 str 本身。
  • 二维数组参数与二维指针参数

    数组参数                         等效的指针参数
    数组的数组: char a[3][4]    数组的指针: char (*p)[4]
    指针数组: char *a[5]       指针的指针: char **p      

    tips:1.C 语言中,当一维数组作为函数参数的时候,编译器总是把它解析
    成一个指向其首元素首地址的指针。这条规则并不是递归的,也就是说只有一维数组才是
    如此,当数组超过一维时,将第一维改写为指向数组首元素首地址的指针之后,后面的维
    再也不可改写。比如: a[3][4][5]作为参数时可以被改写为( *p) [4][5]。
    2.作为参数时,一维数组“ []”号内的数字完全可以省略:void fun( char a[ ][4]),不过第二维的维数不可省略。

  • 函数指针
    形如: char (fun1)(char p1,char p2);

    (*(void(*) ())0)()
    第一步: void(*) (),可以明白这是一个函数指针类型。这个函数没有参数,没有返回值。
    第二步: (void(*) ())0,这是将 0 强制转换为函数指针类型, 0 是一个地址,也就是说一
    个函数存在首地址为 0 的一段区域内。
    第三步: (*(void(*) ())0),这是取 0 地址开始的一段内存里面的内容,其内容就是保存
    在首地址为 0 的一段区域内的函数。
    第四步: (*(void(*) ())0)(),这是函数调用。
    
  • 引用和指针的区别

C++中的引用:
1.概念
 引用就是某一变量(目标)的一个别名,对引用的操作与对变量直接操作完全一样。
 其格式为:类型 &引用变量名 = 已定义过的变量名。
2.特点:
 ① 一个变量可取多个别名。
 ② 引用必须初始化。
 ③ 引用只能在初始化的时候引用一次 ,不能更改为转而引用其他变量。

  • 引用和指针的区别和联系(笔试热点)
      1. 引用只能在定义时初始化一次,之后不能改变指向其它变量(从一而终);指针变量的值可变。
      2. 引用必须指向有效的变量,指针可以为空。
      3. sizeof指针对象和引用对象的意义不一样。sizeof引用得到的是所指向的变量的大小,而sizeof指针是对象地址的大小。
      4. 指针和引用自增(++)自减(--)意义不一样。
      5. 相对而言,引用比指针更安全。
    
    eg:
  • 普通引用
      a = 2;  
      b = 3;  
      int& c = b;// 引用一个引用变量,别名的别名  
    
  • const 引用
      const int & d6 = 5;//常量具有常性,只有常引用可以引用常量  
    
  • 引用传参
    1.【值传递】如果形参为非引用的传值方式,则生成局部临时变量接收实参的值  
    void Swap (int left, int right) //值传递的方式无法实现交换,因为传参时对于参数left和right拷贝一临时副本,交换的是副本值,因为其是临时变量函数退出,变量销 {                                //毁,并不会影响外部left和right的值。  
       int temp = left;  
       left = right ;  
       right = temp ;  
    }  
    2.【引用传递】如果形参为引用类型,则形参是实参的别名。  
    void Swap (int& left, int& right)//使用引用的话,不做临时拷贝,&的使用说明此处只是原参数的另一个名字而已,所以修改时直接在原参数的基础上修改变量值。  
    {  
       int temp = left;  
       right = left ;  
       left = temp ;  
    }  
    3.【指针传递】  
    void Swap (int* pLeft, int* pRight)//传入的是地址,因为地址是唯一的,所以指针通过地址的访问进而可修改其内容。  
    {  
       int temp = *pLeft;  
       *pLeft = *pRight;  
       *pRight = temp;  
    }  
    

- tips:引用和指针的区别和联系:

★不同点:
 1. 指针是一个实体,而引用仅是个别名;
 2. 引用使用时无需解引用(*),指针需要解引用;
 3. 引用只能在定义时被初始化一次,之后不可变;指针可变;
 4. 引用没有 const,指针有 const;const修饰的指针不可变;
 5. 引用不能为空,指针可以为空;
 6. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身(所指向的变量或对象的地址)的大小;
 7. 指针和引用的自增(++)运算意义不一样;
 8. 从内存分配上看:程序为指针变量分配内存区域,而引用不需要分配内存区域。

★相同点:两者都是地址的概念,指针指向一块儿内存,其内容为所指内存的地址;引用是某块儿内存的别名。

详情请见:http://blog.csdn.net/xiao__tian__/article/details/51814617

内存管理

  • 1. 野指针

  • 2. C语言内存分配

(1)代码区(text segment)。代码区指令根据程序设计流程依次执行,对于顺序指令,则只会执行一次(每个进程),如果反复,则需要使用跳转指令,如果进行递归,则需要借助栈来实现。
代码区的指令中包括操作码和要操作的对象(或对象地址引用)。如果是立即数(即具体的数值,如5),将直接包含在代码中;如果是局部数据,将在栈区分配空间,然后引用该数据地址;如果是BSS区和数据区,在代码中同样将引用该数据地址。
(2)全局初始化数据区/静态数据区(Data Segment)。只初始化一次。
(3)未初始化数据区(BSS)。在运行时改变其值。
(4)栈区(stack)。堆栈指的就是栈区。由编译器自动分配释放,存放函数的参数值、局部变量的值等。
其操作方式类似于数据结构中的栈。每当一个函数被调用,该函数返回地址和一些关于调用的信息,比如某些寄存器的内容,被存储到栈区。然后这个被调用的函数再为它的自动变量和临时变量在栈区上分配空间,这就是C实现函数递归调用的方法。
(5)堆区(heap)。用于动态内存分配。堆在内存中位于bss区和栈区之间。一般由程序员分配和释放,若程序员不释放,程序结束时有可能由OS回收。

tips:堆栈的区别:
(1)管理方式不同。
栈编译器自动管理,无需程序员手工控制;而堆空间的申请释放工作由程序员控制,容易产生内存泄漏。
(2)空间大小不同。
栈是向低地址扩展的数据结构,是一块连续的内存区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,当申请的空间超过栈的剩余空间时,将提示溢出。因此,用户能从栈获得的空间较小。
堆是向高地址扩展的数据结构,是不连续的内存区域。因为系统是用链表来存储空闲内存地址的,且链表的遍历方向是由低地址向高地址。由此可见,堆获得的空间较灵活,也较大。栈中元素都是一一对应的,不会存在一个内存块从栈中间弹出的情况。
(3)是否产生碎片。
对于堆来讲,频繁的malloc/free(new/delete)势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低(虽然程序在退出后操作系统会对内存进行回收管理)。对于栈来讲,则不会存在这个问题。
(4)增长方向不同。
堆的增长方向是向上的,即向着内存地址增加的方向;栈的增长方向是向下的,即向着内存地址减小的方向。
(5)分配方式不同。
堆都是程序中由malloc()函数动态申请分配并由free()函数释放的;栈的分配和释放是由编译器完成的,栈的动态分配由alloca()函数完成,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行申请和释放的,无需手工实现。
(6)分配效率不同。
栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行。
堆则是C函数库提供的,它的机制很复杂,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大的空间,如果没有足够大的空间(可能是由于内存碎片太多),就有需要操作系统来重新整理内存空间,这样就有机会分到足够大小的内存,然后返回。显然,堆的效率比栈要低得多。
  • 3. 野指针
    两层含义:
    1.就是没有被初始化过的指针。
     程序里定义了一个指针而又没有给这个指针一个具体的地址指向时,这样的指针就是一个野指针。野指针不是NULL指针,是指向“垃圾”内存的指针。人们一般不会错用NULL指针,因为用if语句很容易判断。但是“野指针”是很危险的,if语句对它不起作用。

    2.是指针最初指向的内存已经被释放了的一种指针。 (也称悬空指针)
     它指向不再存在的对象。虽然指针变量仍包含有效的内存地址,但该地址中的数据不再有效,原因通常是该地址已通过调用free()而释放。实际上,这一存储单元可能已被重新分配作其他用途。悬空指针的存在所带来的固有风险是,该存储单元中的新数据可能会被旧指针的拥有者破坏。

  • 4. 函数的入口校验
    一般在函数入口处使用 assert(NULL != p)对参数进行校验。在非参数的地方使用
    if( NULL != p)来校验。但这都有一个要求,即 p 在定义的同时被初始化为 NULL 了。

tips:assert 是一个宏,而不是函数,包含在 assert.h 头文件中。如果其后面括号里的值为假,
则程序终止运行,并提示出错;如果后面括号里的值为真,则继续运行后面的代码。这个宏
只在 Debug 版本上起作用,而在 Release 版本被编译器完全优化掉,这样就不会影响代码。
的性能。

  • 常见的内存错误
  1. 指针没有指向一块合法的内存
     结构体成员指针未初始化
     没有为结构体指针分配足够的内存
     使用指针之前一定要确保指针是有效的
    
  2. 为指针分配的内存太小
  3. 内存分配成功,
  4. 内存越界
  5. 内存泄漏
  6. 内存已经被释放了,但是继续通过指针来使用
     第一种:free( p)之后,继续通过 p 指针来访问内存。
     第二种:函数返回栈内存。这是初学者最容易犯的错误。
     第三种:内存使用太复杂,弄不清到底哪块内存被释放,哪块没有被释放。
    
  • malloc中遇到的问题:

    tips:1.malloc 申请 0 字节内存,函数并不返回 NULL,而是返回一个正常的内存地址。但是你却无法使用这块大小为 0 的内存。
    2.既然使用 free 函数之后指针变量 p 本身保存的地址并没有改变, 那我们就需要重新把 p
    值变为 NULL: p = NULL;
    3.strlen() 函数计算的是 mesg 字符串的长度,不包括"\0".而sizeof()是测字符串长度包括"\0".只有字符串常量才有结束标志符。比如下面这种写法就没有结束标志符了:char a[7] = {‘a’,’b’,’c’,’d’,’e’,’f’,’g’};
    4.memset( a,0,sizeof(a)) ;
    memset 函数有三个参数,第一个是要被设置的内存起始地址;第二个参数是要被设置的值;
    第三个参数是要被设置的内存大小,单位为 byte。

    函数

    1.函数命名要恰当,顺序要合理。
    例如编写字符串拷贝函数str_copy,它有两个参数,不要把把参数名字起为str1 和str2
    数的顺序要遵循程序员的习惯。一般地,应将目的参数放在前面,源参数放在后面
    2.不要省略返回值的类型,如果函数没有返回值,那么应声明为void 类型。
    如果没有返回值,编译器则默认为函数的返回值是int类型的。
    3.return 语句不可返回指向“栈内存”的“指针”,因为该内存在函数体结
    束时被自动销毁。

  • 常见字母ASCI码表
    'A'65  ‘a’97  '0' 48


版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xiaob_bai/article/details/79956592

软件复用为软件开发提速

软件复用为软件开发提速四木(本文转载自软件工程专家网www.21cmm.com)    随着社会信息化程度的提高,IT技术已经深入到社会生活的方方面面,而软件作为IT技术应用的核心,其重要性也日渐突出...
  • gigix
  • gigix
  • 2002-04-09 09:37:00
  • 1374

C语言知识点完美总结

C语言最重要的知识点 总体上必须清楚的: 1)程序结构是三种: 顺序结构 、选择结构(分支结构)、循环结构。 2)读程序都要从main()入口, 然后从最上面顺序往下读(碰到循环做循环,碰...
  • the_Sixth_String
  • the_Sixth_String
  • 2016-10-08 14:26:40
  • 3972

对C语言指针的总结

指针(Pointer)就是内存的地址,C语言允许用一个变量来存放指针,这种变量称为指针变量。指针变量可以存放基本类型数据的地址,也可以存放数组、函数以及其他指针变量的地址。 程序在运行过程中需要...
  • u012507643
  • u012507643
  • 2016-10-25 09:11:36
  • 262

c语言课程总结

这个星期的c语言课程学习结束了,学习c是因为很多语言都具有c的影子,作为前端学习,虽然不是后端,但是对后端语言的学习也是必不可少的,而且前端学习涉及的东西太广泛,所以,拥有一个编程的思维,和一个良好的...
  • qq535972008
  • qq535972008
  • 2015-09-23 14:35:40
  • 843

数据结构(c语言版)总结

1. 数据结构的4中基本类型 1、集合 2、线性结构 3、树形结构 4、图、网状结构 2. 结构定义中的关系描述是数据元素之间的逻辑关系,因此叫逻辑结构 3. 数据存储结构:顺序存储结构、链式...
  • Li__YingYing
  • Li__YingYing
  • 2013-06-13 17:05:59
  • 1172

C语言学习总结(一)

在千锋IOS培训的第一阶段结束了,回顾这段时间还是很有收获的。有时候我会想,相对与我的大学生涯相比,我对C语言对OOP思想都有了更深的理解和使用技巧,在能力上还是有长进的。如下做各章节知识点回顾,完全...
  • yangbo_hbzjk
  • yangbo_hbzjk
  • 2012-11-16 16:07:22
  • 1353

C语言函数总结

今天学习的是C语言的函数,主要讲了函数递归,说实话我今天没有听的很明白,出的题目也有两道没做出来,但还是做个总结吧: 函数的定义,它是由返回值,函数名和叁数组成,其中返回值很重要,然后函数又是有四种...
  • xhp513454480
  • xhp513454480
  • 2016-08-02 17:20:02
  • 206

C语言总结-个人学习笔记

C语言总结l         对浮点类型数据的处理1.         进行大小比较:直接比较可能产生不精确现象,可用极限思想,fabs( a – b ) ε,但要小心处理好精度,因为当数字很大时可能...
  • stephane
  • stephane
  • 2008-03-11 00:16:00
  • 692

C语言基础知识梳理总结

C语言是当代人学习及生活中的必备基础知识,应用十分广泛,下面为大家带来C语言基础知识梳理总结,C语言零基础入门绝对不是天方夜谭!...
  • g984160547
  • g984160547
  • 2017-03-06 09:54:48
  • 8358

C语言项目总结

C语言项目总结 历时14天的项目与答辩总算是完成了
  • zoulang7
  • zoulang7
  • 2015-12-27 23:32:45
  • 286
收藏助手
不良信息举报
您举报文章:C语言总结
举报原因:
原因补充:

(最多只允许输入30个字)