C语言

计算机和编程语言

  1. 解释性语言vs编译语言:语言本无解释/编译之分,只是常用的执行方式而已,比如我们常说C语言是编译性语言,是因为大多数时候我们运行C语言都是先编译后运行,但是其实也是有C语言解释器存在的。
  2. C语言发展与版本
    • 1989年ANSI发布了第一个标准-ANSI C
    • 1990年ISO接受了ANCI的标准-C89
    • C的标准在1995年和1999年两次更新-C95和C99
    • 本课程使用C99版本教学

变量

变量名

  1. 变量名在C语言里面属于标识符(identifier),命名有严格的规范。
    • 只能由字母(包括大写和小写)、数字和下划线(_)组成。
    • 不能以数字开头。
    • 长度不能超过63个字符。
  2. 变量名区分大小写,star、Star、STAR都是不同的变量。
  3. 并非所有的词都能用作变量名,有些词在C语言里面有特殊含义,比如intreturn,它们是C语言的关键字。

变量的声明

  1. C语言的变量,必须先声明后使用。如果一个变量没有声明,就直接使用,会报错。每个变量都有自己的类型(type)。声明变量时,必须把变量的类型告诉编译器。int height;

变量的赋值

  1. C语言会在变量声明时,就为它分配内存空间,但是不会清除内存里面原来的值。这导致声明变量以后,变量会是一个随机的值。所以,变量一定要赋值以后才能使用。
  2. 赋值操作通过赋值运算符(=)完成。下面示例中,第一行声明了一个整数变量num,第二行给这个变量赋值。
    1
    2
    int num;
    num = 42;
  3. 变量的声明和赋值,也可以写在一行。比如int num = 42;。多个相同类型变量的赋值,可以写在同一行。比如int x = 1, y = 2;
  4. 注意,赋值表达式有返回值,等于等号右边的值。
    1
    2
    3
    4
    int x, y;

    x = 1;
    y = (x = 2 * x);
    上面代码中,变量y的值就是赋值表达式(x = 2 * x)的返回值2。
    由于赋值表达式有返回值,所以 C 语言可以写出多重赋值表达式。
    1
    2
    3
    int x, y, z, m, n;

    x = y = z = m = n = 3;
    上面的代码是合法代码,一次为多个变量赋值。赋值运算符是从右到左执行,所以先为n赋值,然后依次为m、z、y和x赋值。

运算符

算术运算符

  1. 算术运算符专门用于算术运算,主要有下面几种。

+:正值运算符(一元运算符),结合关系:自右向左
-:负值运算符(一元运算符),结合关系:自右向左
+:加法运算符(二元运算符)
-:减法运算符(二元运算符)
*:乘法运算符
/:除法运算符
%:余值运算符
=:赋值运算符,结合关系:自右向左
2. 我们可以看到,+-既可以做一元运算符,也可以做二元运算符。做一元运算符的时候,指的是为一个值取正或取负,并且做一元运算符的时候,与赋值运算符一样,结合关系是自右向左
3. 如果变量对自身的值进行算术运算,C 语言提供了简写形式,允许将赋值运算符和算术运算符结合成一个运算符。+= -= *= /= %=。由于赋值运算符的优先级最低,所以当出现a+=b+3时,C语言会先计算b+3的结果,然后计算a = a + (b+3)的结果

自增运算符,自减运算符

  1. C 语言提供两个运算符,对变量自身进行+ 1和- 1的操作。++:自增运算符;–:自减运算符
    1
    2
    i++; // 等同于 i = i + 1
    i--; // 等同于 i = i - 1
    这两个运算符放在变量的前面或后面,结果是不一样的。++var和–var是先执行自增或自减操作,再返回操作后var的值;var++和var–则是先返回操作前var的值,再执行自增或自减操作。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    int i = 42;
    int j;

    j = (i++ + 10);
    // i: 43
    // j: 52

    j = (++i + 10)
    // i: 44
    // j: 54

关系运算符

  1. C 语言用于比较的表达式,称为“关系表达式”(relational expression),里面使用的运算符就称为“关系运算符”(relational operator),主要有下面6个。

    大于运算符
    < 小于运算符
    = 大于等于运算符
    <= 小于等于运算符
    == 相等运算符
    != 不相等运算符

  2. 关系表达式通常返回0或1,表示真伪。C语言中,0表示伪,所有非零值表示真。比如,20 > 12返回1,12 > 20返回0。
  3. 关系运算符不宜连用:(i < j) < k。这个式子中,i < j返回0或1,所以最终是0或1与变量k进行比较。如果想要判断变量j的值是否在i和k之间,应该使用下面的写法:i < j && j < k

逻辑运算符

  1. 逻辑运算符提供逻辑判断功能,用于构建更复杂的表达式,主要有下面三个运算符。
    !:否运算符(改变单个表达式的真伪)。
    &&:与运算符(两侧的表达式都为真,则为真,否则为伪)。
    ||:或运算符(两侧至少有一个表达式为真,则为真,否则为伪)。
  2. 下面是 否运算符 的例子。
    1
    2
    if (!(x < 12))
    printf("x is not less than 12\n");
    上面示例中,由于否运算符!具有比<更高的优先级,所以必须使用括号,才能对表达式x < 12进行否运算。当然,合理的写法是if (x >= 12),这里只是为了举例。
  3. 对于逻辑运算符来说,任何非零值都表示真,零值表示伪。比如,5 || 0会返回1,5 && 0会返回0。
  4. 逻辑运算符还有一个特点,它总是先对左侧的表达式求值,再对右边的表达式求值,这个顺序是保证的。如果左边的表达式满足逻辑运算符的条件,就不再对右边的表达式求值。这种情况称为“短路”。

逗号运算符

  1. 逗号运算符用于将多个表达式写在一起,从左到右依次运行每个表达式。x = 10, y = 20;。上面示例中,有两个表达式(x = 10和y = 20),逗号使得它们可以放在同一条语句里面。
  2. 逗号运算符返回最后一个表达式的值,作为整个语句的值。
    1
    2
    int x;
    x = 1, 2, 3;
    上面示例中,逗号的优先级低于赋值运算符,所以先执行赋值运算,再执行逗号运算,变量x等于1。
    1
    2
    int x;
    x = (1, 2, 3);
    上面示例中,由于有了括号的存在,先执行逗号运算,我们使用最右边的值作为表达式的值,然后赋给x,变量x等于3。

运算符优先级

  1. 运算符的优先级顺序很复杂。下面是部分运算符的优先级顺序(按照优先级从高到低排列)。https://en.cppreference.com/w/c/language/operator_precedence
    圆括号(())从左到右
    所有单目运算符:自增运算符(++),自减运算符(–),一元运算符(+和-),否运算符(!)
    乘法(*),除法(/),取余(%)
    加法(+),减法(-)
    关系运算符(<、>等)
    == !=
    &&
    ||
    赋值运算符(=),其他赋值运算符(+=, -=)
    逗号运算符(,)

流程控制

  1. https://wangdoc.com/clang/flow-control.html
  2. 老师有一点讲的很好:switch-case语句中,case并不是用来分割语句的,case只是用来决定语句从什么地方开始执行。我们在使用switch-case语句的时候,可以无视case,把剩下的所有语句当成一段要执行的代码,case只是选择从哪一句开始执行,然后程序会一直执行下面的代码,直到遇到break为止

数据类型

函数

  1. 函数声明:C语言中的函数在使用前必须先进行定义,这点与Java不同,Java的函数可以定义在任何位置。如果C语言想要在未定义函数前就进行使用,那么就需要使用函数声明。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int twice(int);//函数声明

    int main(int num) {
    return twice(num);
    }

    int twice(int num) {
    return 2 * num;
    }
  2. 调用函数时给的值与参数的类型不匹配是C语言传统上的最大的漏洞,编译器总是悄悄的替你把类型转换好,但是这很可能不是我们所期望的,后续的语言,比如C++和Java在这方面很严格。
    1
    2
    3
    4
    5
    6
    7
    void test(int t) {
    printf("%d", t);
    }

    int main() {
    test(2.4);
    }
    上面的程序编译会通过,虽然会有一个warning,这种程序对于Java根本不可能通过编译
    hello.c:8:10: warning: implicit conversion from ‘double’ to ‘int’ changes value from 2.4 to 2 [-Wliteral-conversion]
     test(2.4);
     ~~~~ ^~~
    
    1 warning generated.
  3. C语言在调用函数的时候,永远只能传值给函数
    值传递(pass by value)是指在调用函数时将实际参数复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实际参数。
    引用传递(pass by reference)是指在调用函数时将实际参数的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实际参数。
    更精辟的理解:操作的是一块内存(引用传递)还是新开辟了一块内存(值传递)的区别
    所以我们看到下面的程序是不会有效的:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    void Swap(int x, int y) {
    int temp;
    temp = x;
    x = y;
    y = temp;
    }

    int a = 1;
    int b = 2;
    Swap(a, b); // 无效
    对于Java来说:Java中本质上是值传递的,只不过对于对象参数(非primitive的所有对象),值的内容是对象的引用(地址),通俗的来说,如果Java中函数的参数是一个对象,那么当我们call这个函数的时候,传进去的是这个对象引用(地址)的一份拷贝。https://www.zhihu.com/question/31203609/answer/576030121
  4. 本地变量: 本地变量只在当前block中可用(函数参数也是本地变量),程序进入这个块之前,其中的变量不存在,离开这个块,其中的变量就消失了。如果在块里面定义了和外面同名的变量,则掩盖掉外面的(这点Java是不支持的)

数组

  1. https://wangdoc.com/clang/array.html
  2. 注意,如果引用不存在的数组成员(即越界访问数组),并不会报错,所以必须非常小心。
    1
    2
    int scores[100];
    scores[100] = 51;
    上面示例中,数组scores只有100个成员,因此scores[100]这个位置是不存在的。但是,引用这个位置并不会报错,会正常运行,使得紧跟在scores后面的那块内存区域被赋值,而那实际上是其他变量的区域,因此不知不觉就更改了其他变量的值。这很容易引发错误,而且难以发现。
  3. 把一个数组的所有元素交给另一个数据,不能直接int a[] = {0, 1, 2}; int b[] = a;,而是只能遍历,后面介绍指针后会介绍具体原因。
  4. 求数组的长度:sizeof(a)/sizeof(a[0])
  5. 数组作为函数参数的时候,我们无法直接通过上面的求数组长度的方法来得到它的长度,所以往往需要再用另一个参数来传入数组的大小,其原因是因为数组作为参数,传入的其实只是数组第一个值的地址,所以我们是没法知道长度的。这点与Java是不同的,Java的数组传入函数的是一个引用,可以直接使用a.length得到数组长度。

指针

  1. 运算符&:获得变量的地址,它的操作数必须是变量
    1
    2
    int x = 1;
    printf("x's address is %p\n", &x);
    上面示例中,x是一个整数变量,&x就是x的值所在的内存地址。printf()的%p是内存地址的占位符,可以打印出内存地址
  2. 变量地址的长度在不同的编译器架构中是不同的,32位编译器架构的变量地址的长度是4个字节,,64位编译器架构的变量地址的长度是8个字节。所以变量地址并不总是和int(4个字节)的长度相同,我们也不应该使用int来表示地址。
  3. &只能取变量的地址,对于表达式来说,是没有地址可以取的,所以下面都是错误的表示:&(a+b); &(a++); &(++a)
  4. 前面我们说过,一个变量的地址并不总是与int长度相等,所以我们需要一个用来储存地址的方法,指针就是保存地址的变量
    1
    2
    3
    4
    int i;
    int* p = &i;
    int* p,q;
    int *p,q;
    上面的例子中,第三四行代表的都是p是一个指针,q只是一个int型的变量。所以,在C语言中,没有int*这种类型,而应该表达为*p是一个int类型的变量。如果要表示p和q都是指针,应该写为int *p, *q;
  5. 运算符*:用来访问指针的值所表示的地址上的变量
    1
    2
    3
    void increment(int* p) {
    *p = *p + 1;
    }
    上面示例中,函数increment()的参数是一个整数指针p。函数体里面,*p就表示指针p所指向的那个值。对*p赋值,就表示改变指针所指向的那个地址里面的值。
    上面函数的作用是将参数值加1。该函数没有返回值,因为传入的是地址,函数体内部对该地址包含的值的操作,会影响到函数外部,所以不需要返回值。事实上,函数内部通过指针,将值传到外部,是C语言的常用方法。
  6. 指针的作用:1.在函数中传入地址,那么我们修改改地址指向的变量,也就可以改变外面的变量,所以在C语言中写一个swap函数必须要传入地址。之前我们提到Java语言也是传值的,那么如果要写一个swap函数交换两个int值,那么往往需要借助数组或者对象,因为Java不提供指针。 2.当我们的函数需要多个返回值的时候,我们可以通过传入多个指针并把返回值写入这些指针指向的变量来达到这个目的 3.函数返回运算的状态,而运算的结果通过指针返回。由于C语言没有异常处理机制,所以我们的程序需要返回一个特殊值来说明是否运行正常,那么运算的结果就无法通过返回值来返回了,这时候可以使用指针。
  7. 指针最常见的错误:定义了指针变量,但是还没有指向任何变量(初始化),就开始使用
    1
    2
    int *p;
    *p = 12;
    这里p储存的地址完全是随机的,而我们想要改一个随地地址的变量的值。
  8. 数组的地址:数组是一连串连续储存的同类型值,只要获得起始地址(首个成员的内存地址),就能推算出其他成员的地址。请看下面的例子。
    1
    2
    3
    4
    int a[5] = {11, 22, 33, 44, 55};
    int* p;
    p = &a[0];
    printf("%d\n", *p); // Prints "11"
    上面示例中,&a[0]就是数组a的首个成员11的内存地址,也是整个数组的起始地址。反过来,从这个地址(*p),可以获得首个成员的值11。
    由于数组的起始地址是常用操作,&array[0]的写法有点麻烦,C 语言提供了便利写法,数组名等同于起始地址,也就是说,数组名就是指向第一个成员(array[0])的指针
  9. 指针与数组:前面我们提到过,数组传入函数中,我们是不能用参数列表的数组变量去计算数组的长度的,原因就是数组传入函数的时候被decay成一个指针,指向数组的起始地址,所以如果对函数内的数组变量使用sizeof,得到的并不是数组的字节长度,而是数组起始地址端的字节长度。这也是为什么在C语言中,当我们想要将一个数组传入函数的时候,我们还需要传入数组的长度,因为传入的参数只是一个指针,我们还需要另外一个变量来指明数组的长度
  10. []运算符可以对数组做,也可以对指针做;*运算符可以对指针做,也可以对数组做
  11. 数组与指针并不等价,只不过数组可以decay成指针来使用:不等价,数组能隐式转换成指针罢了。看到有书这么写的话应该考虑直接扔掉。数组和指针的区别应该是十分基础而显然的。定义一个指针对象 T *ptr; 后 ptr 这个对象里面没有 T 类型对象,不过可能可以通过 ptr 访问存在于别处的 T 类型对象。定义一个数组对象 T arr[N]; 后 arr 这个对象里面有 N 个 T 类型对象。将 arr 隐式转换成指针后,能访问的 T 类型对象是 arr 里面的对象。https://www.zhihu.com/question/362176701/answer/956286999
  12. 指针与const:
    • 指针是const,表示一旦得到了某个变量的地址,不能在指向其他变量,但是已经指向的变量是可以改变的。数组其实就是一个const指针,int[] a等价于int * const a,这也就是为什么前面我们提到数组之间不能互相赋值(int[] a = b是不允许的),因为这是在把一个const指针指向另一个变量。
      1
      2
      3
      int * const q = &i;
      *q = 26; //ok
      q++; //error
    • 所指是const,表示不能通过这个指针去修改那个变量(并不能使得那个变量成为const,也就是说那个变量本身是可以改变的,但你不能用这个指针去改变它)
      1
      2
      3
      4
      const int *p = &i;
      *p = 26; //error!(*p)是const
      i = 26;//ok
      p= &j;//ok
  13. 判断哪个被const了的标志是const在*的前面还是后面:
    1
    2
    3
    4
    int i;
    const int* p1 = &i;//所指是const
    int const* p2 = &i;//所指是const
    int *const p3 = &i;//指针是const
  14. const数组: const int a[] = {1, 2, 3, 4, 5}。数组变量已经是const的指针了,这里的const表明数组的每个单元都是const int,无法通过a[i]进行修改,所以必须通过初始化进行赋值。
  15. 指针运算:对于指针的加1与减1并不是地址的加1或减1,而是表示指针的移动,移动的距离是取决于变量的大小,比如在32位编译器中int的大小是4,如果我们有一个int型的数组array[],现在我们有int *p = array;,那么*(p+n)array[n]是等价的。由于运算符*+的优先级高,所以这里我们需要括号。
  16. 0地址:操作系统会为每个程序虚拟化一段内存,这些内存都有地址为0的一个的地址,但是这个0地址处储存着我们不能操作的数据,因此我们可以把用0指针代表返回的指针是无效的或者指针还没有被初始化(先初始化为0)。C语言中NULL是一个预定义的符号,代表0地址。
  17. 指针的类型:无论指针指向什么类型,所有的指针的大小都是一样的(大小取决于编译器)。但是指向不同类型的指针是不能直接互相赋值的,比如不能把一个int型的指针赋给double型的指针
  18. 指针的类型转换:
    1
    2
    3
    int i = 5;
    int *p = &i;
    void *q = (void*)p;
    void*表示不知道指向什么东西的指针。这里把一个指向int的指针转化成void*的指针,但是这并不会改变p所指的变量的类型,他所指的仍旧是一个int型变量,只是这个时候我们用不同的眼光看待这个变量
  19. 动态内存分配:假设我们的程序中要定义一个长度为n的数组,这个n是通过命令行输入的,这个时候我们可以使用变量n作为数组大小,但是这个feature是C99才引进的,之前ANSIC并不支持变量作为数组长度,那么我们该怎么做呢?答案是使用动态内存分配,我们可以使用malloc函数分配一段长度为n*sizeof(int)的内存:int *a = (int*)malloc(n*sizeof(int))。malloc函数的输入是分配内存的字节长度,malloc函数返回的是void*,我们使用强制类型转化变成int型的数组。malloc函数需要引入#include <stdlib.h>malloc函数用完以后一定要用free函数释放内存、如果内存地址不够分配,malloc函数返回NULL

字符串

  1. C语言没有单独的字符串类型,字符串被当作字符数组,即char类型的数组。比如,字符串”Hello”是当作数组{'H', 'e', 'l', 'l', 'o'}处理的。
    编译器会给数组分配一段连续内存,所有字符储存在相邻的内存单元之中。在字符串结尾,C语言会自动添加一个全是二进制0的字节,写作\0字符,表示字符串结束。字符\0不同于字符0,前者的 ASCII 码是0(二进制形式00000000),后者的 ASCII 码是48(二进制形式00110000)。所以,字符串“Hello”实际储存的数组是{'H', 'e', 'l', 'l', 'o', '\0'}
  2. 字符\0标志字符串的结束,但它不是字符串的一部分,我们计算字符串的长度的时候不包含这个0。其实这点很好理解,这个0只是因为C语言不提供字符串类型,所以需要有一种方式去标记字符串的结束。
  3. 如果字符串过长,可以在需要折行的地方,使用反斜杠(\)结尾,将一行拆成多行。
    1
    2
    "hello \
    world"
    上面示例中,第一行尾部的反斜杠,将字符串拆成两行。
    上面这种写法有一个缺点,就是第二行必须顶格书写,如果想包含缩进,那么缩进也会被计入字符串。为了解决这个问题,C 语言允许合并多个字符串字面量,只要这些字符串之间没有间隔,或者只有空格,C 语言会将它们自动合并。
    1
    2
    3
    char greeting[50] = "Hello, ""how are you ""today!";
    // 等同于
    char greeting[50] = "Hello, how are you today!";
    这种新写法支持多行字符串的合并。
    1
    2
    3
    char greeting[50] = "Hello, "
    "how are you "
    "today!";
  4. 字符串在内存中以字符数组的形式存在,所以不能用运算符对字符串进行操作(不能像Java一样字符串之间相加)。访问的时候可以使用指针或者数组,但是这两者在访问字符串的时候是有区别的。
    1
    2
    3
    4
    // 写法一
    char s[] = "Hello, world!";
    // 写法二
    char* s = "Hello, world!";
    那么这两种声明方式有什么区别呢?首先我们需要知道的是,C语言的字符串字面量(也就是”Hello, world!”)都是生成在代码段的内存中的,这段区域是只可读的,这个时候如果我们尝试s[0] = 'B'是会报错的,并且如果我们用指针方式声明相同的字符串,其实指针是会指向同一个地址的;而使用数组声明的字符串,编译器会给数组在栈区(stack,也就是局部变量和函数参数所在的地方,变量由系统自动分配与销毁)单独分配一段新内存,字符串字面量会被编译器解释成字符数组,逐个字符写入这段新分配的内存之中,而这段新内存是允许修改的,所以s[0] = 'B'是允许的。
  5. 口语中,我们常说用char*定义一个字符串,但是char*声明的并不一定是字符串,也可能只是一个指向单个字符的滋镇,只有当它所指的是一个字符数组并且结尾是一个0的时候,才能说它指的是字符串。
  6. 字符串数组:介绍过字符串以后,那么字符串数组应该如何表示呢?可能的方案有:1.char ** a 2.char a[][] 3. char * a[]。方案一并不是字符串数组,而是表示a是一个指针,指向另一个指针,那个指针指向一个字符(串);方案二可以在一定程度上代表字符串数组,但是有一个问题,二维数组的第二个维度必须是确定的,那么这里我们的字符串就会有一个长度限制 3.方案三代表的才是字符串数组,这里数组中只存放char*,也就是指针。
  7. C语言标准库提供了很多处理字符串的函数,我们在使用之前需要先加上头文件#include <string.h>
    • size_t strlen(const char *s):返回字符串长度(不包括结尾的0)
    • int strcmp(const char *s1, const char *s2):比较两个字符串的长度,如果s1==s2,返回0,如果s1>s2,返回1,如果s1<s2,返回-1
    • char * strcpy(char *restrict dst, const char *restrict src):把第二个字符串拷贝到第一个字符串的位置,restricted代表两个字符串不重叠。返回的就是dst这个字符串,目的是为了能链起代码来
    • char * strcat(char *restrict s1, const char *restrict s2):把s2拷贝到s1后面,s1必须有足够的空间。strcat和strcpy都可能出现安全问题,因为目的地可能没有足够的空间,可以使用安全的版本strncpy和strncat,n代表最多能拷贝的字符
    • char * strchr(const char *s, int c); char * strrchr(const char *s, int c):在字符串中找第一个出现的c,没有返回0指针NULL,有的话返回对应指针,strrchr代表从右向左找,strchr代表从左向右找

enum,struct与union

  1. enum:https://wangdoc.com/clang/enum.html
  2. struct:https://wangdoc.com/clang/struct.html
  3. struct赋值:除了逐一对struct的属性赋值,也可以使用大括号,一次性对 struct 结构的所有属性赋值。
    1
    2
    3
    4
    5
    6
    struct car {
    char* name;
    float price;
    int speed;
    };
    struct car saturn = {"Saturn SL/2", 16000.99, 175};
    上面示例中,变量saturn是struct car类型,大括号里面同时对它的三个属性赋值。如果大括号里面的值的数量,少于属性的数量,那么缺失的属性自动初始化为0。
  4. struct复制:struct变量可以使用赋值运算符(=),复制给另一个变量,这时会生成一个全新的副本。系统会分配一块新的内存空间,大小与原来的变量相同,把每个属性都复制过去,即原样生成了一份数据。这一点跟数组的复制不一样,务必小心。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct cat { char name[30]; short age; } a, b;

    strcpy(a.name, "Hula");
    a.age = 3;

    b = a;
    b.name[0] = 'M';

    printf("%s\n", a.name); // Hula
    printf("%s\n", b.name); // Mula
    上面示例中,变量b是变量a的副本,两个变量的值是各自独立的,修改掉b.name不影响a.name。
    上面这个示例是有前提的,就是 struct 结构的属性必须定义成字符数组,才能复制数据。如果稍作修改,属性定义成字符指针,结果就不一样。
    1
    2
    3
    4
    5
    6
    struct cat { char* name; short age; } a, b;

    a.name = "Hula";
    a.age = 3;

    b = a;
    上面示例中,name属性变成了一个字符指针,这时a赋值给b,导致b.name也是同样的字符指针,指向同一个地址,也就是说两个属性共享同一个地址。因为这时,struct 结构内部保存的是一个指针,而不是上一个例子的数组,这时复制的就不是字符串本身,而是它的指针。并且,这个时候也没法修改字符串,因为字符指针指向的字符串是不能修改的。
  5. struct作为函数参数:整个struct可以作为参数传入函数,这个时候是在函数内部新建一个struct并复制传入的struct的值;但是一般当struct很大的时候,常见的做法是传一个struct指针。
    1
    2
    3
    void happy(struct turtle* t) {
    (*t).age = (*t).age + 1;
    }
    上面示例中,(*t).age不能写成*t.age,因为点运算符.的优先级高于*。*t.age这种写法会将t.age看成一个指针,然后取它对应的值,会出现无法预料的结果。
    (*t).age这样的写法很麻烦。C语言就引入了一个新的箭头运算符(->),可以从struct指针上直接获取属性,大大增强了代码的可读性。
    1
    2
    3
    void happy(struct turtle* t) {
    t->age = t->age + 1;
    }
  6. 自定义数据类型:我们可以使用typedef来声明一个已有的数据类型的新名字,比如typedef int Length;。typedef命令也可以为struct结构指定一个别名,这样使用起来更简洁。
    1
    2
    3
    4
    5
    6
    typedef struct cell_phone {
    int cell_no;
    float minutes_of_charge;
    } phone;

    phone p = {5551234, 5};
    上面示例中,phone就是struct cell_phone的别名。
  7. union:https://wangdoc.com/clang/union.html