C语言总结(数组,函数,指针,结构体,预处理)

  1. 变量的声明和定义
    变量声明(declaration) 可以declaration很多次,不占内存空间,例如 extern int a;
    变量定义(define) 定义只能定义一次 int a;(全局变量0;局部变量为任意值)

  2. 数组

    • 数组申请
      int m ;
      int a[m];
      可以运行,但在运行的时候m必须确定

    • 特殊位置赋值
      int f[5] = [[4]=5]; //f[4] = 5,其他位置为0
      int i[] = {7,[6]=10,100,1000}; 一共有9个元素,第7,8,9位的值是10,100,1000.剩下的值为7

    • 数组传参
      int fun(int[] a)
      int fun(int b[][5])//第一唯是大小,剩下的是类型。 传递一个类型为int[5]的数组b

    • 字符串赋值
      char p1[5]={"Hello"}; 编译器最后会自动添加'\0'
      char p2[5]={"H","e","l","l","o"}; 没有‘\0’

    • 二维数组

      • 二维数组初始化:
        int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
        int b[3][2] = {{1, 2}}; = {{1, 2}, {0, 0}, {0, 0}};
        int c[3][2] = {{1}, {2, 3}}; = {{1, 0}, {2, 3}, {0, 0}};

        int d[3][2] = {1, 2, 3, 4, 5, 6}; = {{1, 2}, {3, 4}, {5, 6}};
        int e[3][2] = {1, 2, 3}; = {{1, 2}, {3, 0}, {0, 0}};
        int f[][3] = {1, 2, 3}; = {{1, 2,3}};
        int g[3][] = {1, 2, 3}; 错误

  1. 指针
    • 为什么要用指针?
      计算机是按字节编址的,使用指针是为了方便程序员操作者内存

    • 打印指针地址 使用 p%

    • 数组,指针,函数关注点

      • 数组关注:大小+类型
      • 指针:类型
        • 硬件里面操作 基地址+偏移
      • 函数:参数+返回值
    • 指针数组,数组指针,函数指针

           int *p[3]; //p 往右读是[],则类型是数组,吧p[3]看成一个整体,数组里面存的是int*
           int (*p)[3];//p 遇到‘)’ 向左读 遇到‘*’,则类型是指针,数组指针,p指向一个一个int[3]的数组。
           int (*p)(int a);//p 遇到‘)’ 向左读 遇到‘*’,则类型是指针,往右读遇到'()',p是一个指向参数为int a ,返回值类型为int 的函数。
           int (*p[3])(int a);
      

      解读方法:首先从标示符开始阅读,然后往右读,每遇到圆括号就调转阅读方向。重复这个过程直到整个声明解析完毕。需要注意的是,已经读过的部分在后续作为一个整体来看。

           函数指针:
           int echo(int a)
           {
               return a;
           }
           int main()
           { 
               int (*p)(int a); 
               p = echo;
               printf("echo(5)=%d\n", echo(5));
               printf("p(5)=%d\n", p(5));
               
               return 0;
           }
      
           数组指针:
           int main()
           { 
               int (*p)[3]; 
               int d[2][3] = {1, 10, 1000, 2, 20, 2000};
               p = d;
           
               for(int i=0; i<2; i++)
               {
                   for(int j=0; j<3; j++)
                       printf("%d\t", *(*(p+i)+j));
                   printf("\n");
               }
               return 0;
           }
      
           指针数组:
           int main()
           { 
               int a, b,c;
               a=b=c=10;
               int *p1[3] = {&a, &b, &c};
               for(int i=0; i < 3; i++)
                   printf("%d\t", *(p1[i]));
               printf("\n");
           }
      
           函数指针数组:
           int function1(int a)
           {
               printf("int function1\n");
               return 0;
           }
           int function2(int a)
           {
               printf("int function2\n");
               return 0;
           }
           int function3(int a)
           {
               printf("int function3\n");
               return 0;
           }
      
           int main()
           { 
               int (*p[3])(int a); 
               p[0] = function1;
               p[1] = function2;
               p[2] = function3;
               p[0](10);
               p[1](10);
               p[2](10);
               return 0;
           }
      
    • a,&a,&a[0]

      • 假设:int a[3] = {1, 2, 3};

        1. a代表什么?
          a是整个数组的名字,当a作为右值时,它代表首元素的首地址,等价于&a[0]
        2. &a代表什么?
          &a取a的地址,即整个数组的地址
        3. &a[0]代表什么?
          代表首元素的首地址
        4. a、&a、&a[0]的值相等吗?
          相等
        5. sizeof(a)、sizeof(&a)、sizeof(&a[0])相等吗?分别是多少?
          sizeof(a)=12
          sizeof(&a)=4
          sizeof(&a[0])=4
      1. sizeof(a[4])合法吗?如果合法,值是多少?为什么?
        sizeof(a[4])=4
      2. a+1、&a+1、&a[0]+1分别是多少?指向的值又是多少?
        a+1 = &a[0]+1 指向 2
        &a+1 指向 数组末尾
      3. a[-1]是否合法?如何理解?
      • 程序验证

          #include <stdio.h>
          int main(void)
          {
              int a[3] = {1, 2, 3};
              printf("sizeof(int)=%lu\tsizeof(unsigned long)=%lu\n", sizeof(int), sizeof(unsigned long));
              printf("a=%p\t&a=%p\t&a[0]=%p\n", a, &a, &a[0]);
              printf("sizeof(a)=%lu\tsizeof(&a)=%lu\tsizeof(&a[0])=%lu\n", sizeof(a), sizeof(&a), sizeof(&a[0]));
              printf("sizeof(a[4])=%lu\n", sizeof(a[4]));
              printf("a+1=%p\t&a+1=%p\t&a[0]+1=%p\n", a+1, &a+1, &a[0]+1);
              printf("*(a+1)=%d\t*(&a+1)=%d\t*(&a[0]+1)=%d\n", *(a+1), *(&a+1), *(&a[0]+1));
              printf("a[-1]=%d\n", a[-1]);
              return 0;
          }
        
      • 分析&&结论

        1. a、&a、&a[0]虽然含义不同,但它们的值都是一样的。

        2. a是整个数组的名字,代表整个数组,所以sizeof(a)的值为整个数组的大小。但有一个例外,当a作为右值时,它代表首元素的首地址,即相当于&a[0]。所以a+1和&a[0]+1等价。

        3. &a取a的地址,即整个数组的地址。所以&a+1表示“下一个数组”的地址,而不是下一个数组元素的地址。

        4. &a[0]代表数组第一个元素的地址。

        5. a[-1]是什么含义呢?其实编译器内部是按照指针加偏移的方式来处理数组下标的。比如a[i],编译器会解析为* (a+i),所以a[-1]其实就是*(a-1)的值。根据这个规则我们还会发现其实a[i]和i[a]都是合法的,且指向的是同一个值

        6. 当不知道数组大小时,获取数组最后一个元素。

           int a[5] = {1,2,3,4,5};
           int *p = (int*)(&a + 1);
           *(p-1) 的值是 5
          
    • 指针和数组

      • 指针和数组虽然很相近,我们经常交换使用,但只有在下面两种情况下,二者才是等价的:

        • 在表达式中,对数组下标的引用总是可以换成指向数组起始地址的指针加偏移量(而且编译器一般都是这么做的),也就是说用a[i]这样的形式访问数组时总是被编译器改写成像* (a+1)这样的指针访问。比如:

            int a[] = {1, 3, 5, 7, 9};  
            int *p = a;a[2]等效于*(p+2)  
          
        • 在函数参数的声明中,数组名被编译器当做指向该数组第一个元素的指针。比如下面对于func函数的定义是完全等效的:

            func(int *a) { … }
            func(int a[]) { … }
            func(int a[100]) { … }
          
        • 除了上面这两种情况外,数组就是数组,指针就是指针,不要交换使用。

    • 指针和二维数组

      • 假设定义了二维数组a[m][n],且int *p = a[0],则
      1. a+1代表什么?
        a+1 指向 a[1]

      2. 不使用下标的方式,如何通过a来访问元素a[i][j]?
        a[i][j] = *((p+i)+j)

      3. 行数组指针:
        定义:type (* var_name) [cols]
        动态分配内存:

         int m, n;
         int (*p)[m];// 动态初始化一个n*m的数组
         p = (int(*)[m])malloc(n * m * sizeof(int));  
        
  • const
    1. 定义非指针类型的常量

      const int a;
      a = 5;  // 错误
      const int b = 6;
    
    1. 函数参数

       void print_array1(const int a[], int n)
       {
           for(int i = 0; i < n; i++)
               printf("%d\n", a[i]);
       }
       void print_array2(int a[], int n)
       {
           for(int i = 0; i < n; i++)
               printf("%d\n", a[i]++);
       }
       int main()
       { 
           const int a[3] = {1, 2, 3};
           int b[3] = {4, 5, 6};
           print_array1(a, 3);
           print_array1(b, 3); // print_array2(a, 3);
           print_array2(b, 3); 
           return 0;
       }
      

      函数参数中使用const是为了防止函数内部对函数进行更改

    2. 指针与const
      1. 指针常量:指针是常量,即指针只能指向某个固定地方,不能指向其他地方。

               int a[3] = {1, 2, 3};
               int b[3] = {4, 5, 6};
               int * const pi = a; 
               pi[1] = 2;
               pi = b; // 错误
      
       2. 常量指针:指向常量的指针,指针所指向的内容不可修改,但指针本身可修改(比如,指向其他地址)  
      
               int a[3] = {1, 2, 3};
               int b[3] = {4, 5, 6};
               const int *pi = a; //或 int const *pi = a;
               a[1] = 10;
               pi[1] = 10; // 错误
               pi = b;
      
       3. 区分方法
           去掉类型名,右侧是指针变量名,就是指针常量。
      
    3. 指针和引用

      1. 指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:

         int a=1;int *p=&a;
         
         int a=1;int &b=a;
        

        上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。

        而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。

      2. 可以有const指针,但是没有const引用;

      3. 指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

      4. 指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

      5. 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

      6. "sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;

      7. 指针和引用的自增(++)运算意义不一样;

      8. 指针和引用作为函数参数进行传递时的区别

        1. 指针作为参数进行传递

           #include<iostream>
           using namespace std;
           
           void swap(int *a,int *b)
           {
             int temp=*a;
             *a=*b;
             *b=temp;
           }
           
           int main(void)
           {
             int a=1,b=2;
             swap(&a,&b);
             cout<<a<<" "<<b<<endl;
             system("pause");
             return 0;
           }
          

          输出结果:2 1

           #include<iostream>
           using namespace std;
           
           void test(int *p)
           {
             int a=1;
             p=&a;
             cout<<p<<" "<<*p<<endl;
           }
           
           int main(void)
           {
               int *p=NULL;
               test(p);
               if(p==NULL)
               cout<<"指针p为NULL"<<endl;
               system("pause");
               return 0;
           }
          

          运行结果为:
          0x22ff44 1
          指针p为NULL

          在main函数中声明了一个指针p,并赋值为NULL,当调用test函数时,事实上传递的也是地址,只不过传递的是指地址。也就是说将指针作为参数进行传递时,事实上也是值传递,只不过传递的是地址。当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,即上面程序main函数中的p何test函数中使用的p不是同一个变量,存储2个变量p的单元也不相同(只是2个p指向同一个存储单元),那么在test函数中对p进行修改,并不会影响到main函数中的p的值。

          3.将引用作为函数的参数进行传递。

           #include<iostream>
           using namespace std;
           
           void test(int &a)
           {
             cout<<&a<<" "<<a<<endl;
           }
           
           int main(void)
           {
               int a=1;
               cout<<&a<<" "<<a<<endl;
               test(a);
               system("pause");
               return 0;
           }
          

        输出结果为:
        0x22ff44 1
        0x22ff44 1
        引用作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。

  1. 预处理

    • 预处理器指令从#开始,到其后第一个换行符“\n”为止。也就是说,指令的长度仅限于一行代码。但是,我们经常会看到我们可以使用反斜线“\”将指令扩展到多个物理行,由多个物理行组成一个逻辑行(但是这个并不是C预处理器的特性,而是C编译器的特性:在预处理开始前,编译器会查找反斜线和换行符的组合,并将其删掉)。

    • #define——“函数”
      #define SQUARE(X) ((X)
      (X))

      • 使用时必须要使用足够多的括号来保证宏展开后以正确的顺序进行结合和运算。
      • 不要在宏中使用增量或减量运算符。比如++和--。
    • typedef用途总结

      • 用途1:定义一种类型的别名,而不只是简单的宏替换。可以用作同时声明指针型的多个对象。比如注意一下三种定义的区别与联系:

          char* pa,pb; //等价于  char *pa ; char pb;
          typedef char* PCHAR;
          PCHAR pa,pb; //等价于char *pa,*pb;
        
      • 用途2:定义与平台无关的类型。比如定义一个叫 REAL 的浮点类型,在目标平台一上,让它表示最高精度的类型为:typedef long double REAL; 在不支持long double的平台二上,改为:typedef double REAL; 在连double都不支持的平台三上,改为:typedef float REAL; 也就是说,当跨平台时,只要改下typedef本身就行,不用对其他源码做任何修改。标准库就广泛使用了这个技巧,比如size_t。另外,因为typedef是定义了一种类型的新别名,不是简单的字符串替换,所以它比宏来得稳健(虽然用宏有时也可以完成以上的用途)。

      • 用途3:在旧的C代码中,定义一个结构体类型变量的时候,需要写上struct,即struct 结构名 变量名 。而在C++或新的C中,可以省去struct关键字,直接定义变量。所以在旧的C标准中,我们可以用typedef来实现新的C中的功能。

    • typedef注意点

      • 陷阱1:记住,typedef是定义了一种类型的新别名,不同于宏,它不是简单的字符串替换。

        • 比如:先定义:typedef char* PSTR; 然后:int mystrcmp(const PSTR, const PSTR);const PSTR实际上相当于const char*
          不是的,它实际上相当于 char* const。原因在于const给予了整个指针本身以常量性,也就是形成了常量指针char* const。简单来说,记住当const和typedef一起出现时,typedef不会是简单的字符串替换就行。
      • 陷阱2:typedef在语法上是一个存储类的关键字(如auto、extern、mutable、static、register等一样),虽然它并不真正影响对象的存储特性。比如:

          typedet static int INT2;//不可行
        
    • define vs typedef

      • typedef只是给类型定义别名(准确说是链接),但是#define还可以给常量“定义”别名;
      • typedef的定义是由编译器处理的,而#define是在预处理器处理的
      • typedef是定义了一种新的类型,而#define只是文本替换;
      • typedef遵循作用域规则,而#define没有;
      • typedef定义需要以封号结尾,而#define不需要;
  1. 结构体
    字节对齐

     struct A
     {
        //假设内存地址从0开始...   
        int a;    //0-3
        char b; //4  
        short c;//6-7
     }
     //由于0-7的相加的结果为8...为自对齐4的倍数...
     //所以结果:sizeof(A) = 8
      
     
     struct B
     {
          //假设内存地址从0开始...
         char a;//0
         int b;  //4-7
         short c;//8-10
     }
    

使用#pragma pack更改了字节对齐值

    #pragma pack(2)
    struct C
    {
        //假设从0开始
        char a;//0
        int b;//2-5
        short c;//6-7
    };
    sizeof(C)的答案为8

字节对齐 按最长的对齐 ,如果剩下的不够放 再开辟按最长空间

    struct tagS1
    {
        
        char a;
        int n;
        long l;
        double t1;
        char sz[22];
    };
    //结果为48

下图为struct tagS1在内存中的分布图:

image

推荐阅读更多精彩内容