C-迷路的指针

C语言

基本数据类型

  1. getchar()函数
    1. 只能从键盘缓冲区接收字符,一次只能接收一个字符。如果之前有scanf("%c",&str);类似语句,回车键\n也被会当作一个字符留在键盘缓冲区。如果不是char类型倒不要紧。
    2. 如果之前没有用scanf()接收过字符,那么使用getchar()函数时,需要先键入字符,按enter键后,键入的字符(串)进入缓冲区,然后getchar会从中取一个字符(按输入的顺序),以后每次调用getchar()都会从缓冲区接收一个字符,直至缓冲区字符用完,再重复以上步骤。
    3. getchar()函数的返回值也不是字符而是一个整型.(读取成功时就返回该字符的ASCⅡ值,失败时就返回一个-1。)
    4. 典型例题:加密电文,所有大小写英文字母+4(ASCII码) 循环,其余字符不变。
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      #include <stdio.h>
      int main()
      {
      char str;

      for ( ; (str=getchar()) != '\n' ; ){
      if (str>='a'&& str <= 'z'){
      putchar('a'+(str+4-'a')%26);
      }
      else if (str >= 'A'&& str <= 'Z'){
      putchar('A'+(str+4-'A')%26);
      }
      else{
      putchar(str);
      }
      }
      return 0;
      }
  2. scanf()函数

    1. scanf()函数返回值是是成功读取并赋值的参数的数量。
      scanf()函数返回值分为3种:
      (1). 返回正整数。表示正确输入参数的个数。
      (2). 返回整数0。表示用户的输入不匹配,无法正确输入任何值。
      (3). 返回-1。表示输入流已经结束。在Windows下,用户按下CTRL+Z(会看到一个^Z字符)再按下回车(可能需要重复多次),就表示输入结束;Linux/Unix下使用CTRL+D表示输入结束。

      参考如下例题:计算输入整数满足正确算式的数量.
      上述例题的两种解法:(链接中已给出一种,下给出另一种)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      #include <stdio.h>
      int main(){
      int i,j,d[3];
      int judge = 1,count = 0;

      for (i = 0; judge;){ //判断条件是judge不为0.
      for (j = 0; j<3; j++){
      scanf("%d",&d[j]);
      if (getchar() == '\n'){
      judge = 0; //遇到换行符即表示输入结束。
      }
      }
      if (j == 3 && (d[0] + d[1] == d[2])){
      count++;
      }
      }

      printf("%d\n",count);
      return 0 ;
      }
    2. scanf()也是从键盘缓冲区得到输入,一般来说,遇到换行符\n就表示输入项结束了,但在上述例子计算输入整数满足正确算式的数量中,scanf由于一次性需要接收三个参数,此时换行符\n就不起作用了,需要手动敲ctrl + z再按回车。windows上,ctrl + z就表示输入项的结束。具体可参考详解输入输出流结束标志ctrl+z和EOF.
  3. 逗号表达式
    逗号表达式一般形式为: expr1, expr2, expr3,...,exprn.
    逗号表达式从左到右依次求值,每个表达式的值被忽略,除了最后一个表达式。逗号表达式的值就是最后一个表达式的值。
  4. 运算符优先级及结合性
    结论:先计算优先级大的,相同优先级再根据结合性计算


    观察如下代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <stdio.h>
    int main(){
    int x = 5, y;

    y = 2 * x++;

    printf("x = %d",x);
    printf("y = %d",y);
    return 0;
    }

    结果是:

    x = 6; y = 10;

    于是疑问出现了,自增运算符++优先级不是大于乘*吗?不是先自增再乘嘛,这样y = 2*6 = 12.
    其实:程序确实先进行自增运算符,但++x和x++返回的结果是不同的,然后再赋值给y。特别是x++,它的返回值就是x,而++x的返回值是x + 1, 所以造成一种假象,以为先进行了乘法运算。
    小试牛刀:

    关于str = '!'; 48 <= str <= 57为啥总是得到1?

    因为<=的结合性是自左向右,故先会计算48 <= str,此时str = ‘!’,ASCII值是33,故返回值是0.再计算0 <= 57,返回值是1.由此可以看出学了python之后,再学C感觉步骤很啰嗦。但正是因为步骤啰嗦(分类齐全,条理清晰),故C速度很快。

选择结构程序设计

  1. 注意if - else if - else,按顺序判断,只要其中一个条件为真,剩下的就不会执行!
    如果没有大括号就遵循就近原则,所以写的时候尽量带上大括号。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <stdio.h>
    int main()
    {
    int a = 1,b = 2, c = 3;
    if (a > b)
    if (b > c)
    printf("%d",a);
    else
    printf("%d",b);

    return 0;
    }

    最后结果什么也不会输出!因为其相当于
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    if (a>b)
    {
    if (b > c)
    {
    printf("%d",a);
    }
    else
    {
    printf("%d",b);
    }
    }
  2. switch选择语句:注意表达式A的值必须为整型数据(当然包括字符型),而a、b…必须是常量或者常量表达式。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    switch (表达式A) 
    {
    case a: 表达式1 ; break; // 必须加上break,否则后续case会一直执行,直到break或者全部读完。
    case b: 表达式2 ; break; // 表达式A的值为a\b..时,就执行对应语句。
    case c: { // 也可以写成这样。
    表达式1;
    表达式2;
    break;
    }
    ...
    default: 表达式n ; break; // 可以不用break,反正都结束了。
    }
  3. C语言中唯一一个三目运算符:条件运算符(? :),对应表达式就是条件表达式:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    a > b ? a:b
    //条件语句
    c = a > b ? a:b ;
    //等价于
    if (a>b){
    c = a
    }
    else{
    c = b;
    }

    循环结构程序设计

  4. while循环:表达式为真,进入循环,直至表达式为假或者break;跳出循环.
    1
    2
    3
    4
    5
    while (表达式)
    {
    语句1;
    ...
    }
  5. do while循环:特别注意while后面还有个分号;.
    1
    2
    3
    4
    5
    do
    {
    语句1
    ...
    } while (表达式) ;
  6. for循环:其中表达式1和3可以为逗号表达式,表达式2是判断条件,为真的话继续。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    for (表达式1; 表达式2; 表达式3) 
    {
    语句1;
    ...
    }

    //等价于:
    表达式1 ;
    while (表达式2)
    {
    语句1;
    ...
    表达式3;
    }

数组

数组定义及初始化

一维数组定义:(二维数组同理)

类型符 数组名[常量表达式]

特别注意是常量表达式,不能是变量
但是!C99推出了变长数组(Variable Length Array,VLA),它允许在运行时动态地定义数组的长度,但一旦定义,在其生命周期内大小不可改变。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main() {
int rows, cols;

printf("Enter the number of rows: ");
scanf("%d", &rows);

printf("Enter the number of columns: ");
scanf("%d", &cols);

int matrix[rows][cols];

// 其他操作...

return 0;
}

同时,值得注意的是,VLA不允许在定义的时候初始化
例如int matrix[rows][cols] = {0};,编译器会报错。


二维数组是一维数组线性拓展得到的,也是以线性的方式存储的。
数组名的值数组第一个元素的地址,相当于是一个常量,是不能被赋值的。因此下列数组初始化是错误的:

1
2
3
char a = "string";

int b = {1,2};

字符串输入与输出

如何直接输出字符串?或者输入字符串?

1
2
3
4
5
6
//输出, str是已定义的字符串数组
printf("%s",str);
put(str); // 特别注意put不能输出多个字符串,而printf()可以。
//输入
scanf("%s",str); //由于str的值就是数组第一个元素的地址,故不需要取值符`&`.同时,输入的字符串大小应不大于定义的字符数组的大小。
get(str); //同理,一次只能接收一个字符串数组。

字符串处理函数

函数名称 作用 返回值
puts(str) 输出单个字符串 -
gets(str) 输入单个字符串 -
strlen(str) 测量字符串的长度 返回字符串的长度(不包括\0)
sizeof(str) 测量字符串的内存大小 返回字符串的内存大小(注意,如果字符串没规定大小,如char str[] = "string";,则返回值包括\0,如果规定大小了,则返回的值该大小所占字节。)
补充:sizeof(array) 测量其他类型数组的内存大小 返回其他类型数组的内存大小(此时并没有\0的烦恼了)
strcat(str1, str2) 字符串2接到字符串1后面 返回的是字符串1的地址
strcpy(str1, str2) 将字符串2(包括\0)复制到字符串1中 返回的是字符串1的地址
strncpy(str1, str2, n) 将字符串2前n个字符复制到字符串1中 返回的是字符串1的地址
strlwr(str) 将字符串中大小写字母变成小写字母 不返回任何值,对于字符串是原位修改
strupr(str) 将字符串中大小写字母变成大写字母 不返回任何值,对于字符串是原位修改
strcmp(str1, str2) 依次比较str1和str2中字符的大小,按照ASCII码比较 str1==str2,则返回0;str1 > str2,则返回一个正整数;str1 < str2,则返回一个负整数.

指针

指针与指针变量

指针变量也是一个变量,占用的字节大小取决于其存储的内存地址的大小,而常说的指针应该是指针变量,其值是“内存地址”(指针)。
需要说明的是:后续所讲指针多指指针变量,例如p指向变量a,完整的说法是:p的值是变量a的地址。

前提:int x = 10, *y; y = &x;
重点:可以认为*y == x。

1
2
3
4
5
6
7
8
9
10
11
12
char a = 'A';

//声明指针变量,&为取地址运算符
char *pa = &a; //用char是因为指针变量所储存的内存地址所对应的数据类型是char.

printf("%d",pa); //打印内存地址

printf("%d",*pa); //打印变量a的值,这里*是取值运算符。

//也即:
*(&a) = a;


下面以小甲鱼的一道课后习题来介绍:
Q:请问下边代码执行后,打印机的结果是什么?另外,*b 是左值(l-value)还是右值(r-value)?
1
2
3
4
5
6
7
8
9
#include <stdio.h>
int main()
{
int a = 110;
int *b = &a;
*b = *b - 10;
printf("a = %d\n", a);
return 0;
}

打印结果是 a = 10.

第一个问题:定义指针变量 b 的时候,存放的是变量 a 的地址。在此之后,*b 即对变量 a 的间接访问(通过地址访问 a 变量)。所以 *b = *b - 10; 相当于 a = a - 10; 也就是说,通过指针对一个变量间接访问,你可以理解为把它作为那个变量本身使唤(即 *b == a).

第二个问题:指针变量 b 既是左值,也是右值

此外,关于取址符(&)还有个疑问:一般来说取址符作用对象是左值(lvalue),而数组名不是左值,为啥取址符还可以作用于数组名?
A:其实左值下面有个子集——可修改的左值,位于等式左边必须要求可修改的左值!由此可见,之前一直存在认知误区,其实数组名是左值,只是不可修改罢了。于是有以下结论:

  • 取址符作用于左值,数组名是左值(不可修改的左值),故可将取址符作用于数组名!!!

参考: 数组和指针的区别

一维数组与指针的关系

(不特别说明,数组均指一维数组)
数组名==指针变量,数组名储藏着数组第一个元素的地址,对于字符数据类型,可以直接用指针创建数组,下面代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
int main()
{
char *a = "OUC";
int i;

for (i = 0; i < 3;i++)
{
printf("%c",a[i]); //此处相当于是 a[i] = *(a+i)
}

return 0;
}

然而对于int等类型,通过指针创建是行不通,究其原因,是因为字符数组就是第一个字符的地址(指针变量),且字符数组名和其他数组名一样,也是指针变量,指向数组第一个元素。

char str[] = “string”;
上述str和”string”都可以当作指针变量。

1
2
3
4
5
6
7
#include <stdio.h>
int main(){

printf("%c",*("sting")); //会输出's'.

return 0;
}

通过指针访问数组

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>

int main(){
int a[5] = {1,2,3,4,5};
int *p = a;

printf("*p = %d, *(p+1) = %d.\n",*p, *(p+1));

return 0;
}

结果就是:

*p = 1, *(p+1) = 2.

这就是通过指针间接访问数组的办法,区别于下标直接访问法
此处要注意的是,只有当指针指向数组元素时,指针算术运算才有意义,否则就会给未用地址乱赋值,就会报错(Segmetation default)。
既然数组名也是一个指针变量,那么同理也用数组名进行访问,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(){
int a[5] = {1,2,3,4,5};

printf("*a = %d, *(a+1) = %d.\n",*a, *(a+1)); //这个为后续指针数组做下铺垫,例如int (*ptr)[5] = &a; 这里ptr是一个指针变量,存着数组a的地址,*ptr就是数组a,也就代表着数组第一个元素的地址。从而有
/*
int (*ptr)[5] = &a;
int i;
for (i = 0; i<5; i++){
printf("%d",*(*ptr+i));
}

*/

return 0;
}

⭐⭐⭐同时又可以发现:指针所指向的数据类型决定指针的跨度。

指针与数组的区别

指针变量是左值(lvalue),可以修改的;而数组名是地址常量,不可以修改,故不是左值。
代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int main()
{

int str[] = {1,2,3};
int *target = str;
int count = 0;

while (*target++ != 3) //此处必须用指针变量,不能用数组名。
// 这里*target++是啥呢? 由于增运算符(变量++)的优先级大于取值运算符(*),故先进行target++,再取值*,相当于*(target++)。
//同时,还需注意,自增运算符在变量后面,故取值符(*)取用的是未自增前的值。
{
count++;
}

printf("总共有%d个数字\n",count);

return 0;
}

小甲鱼作业S1E22第2题

请问 str[20] 是否可以写成 20[str]?
A: C 语言中,a[b] 被解释为 *(a + b),故两者等价。

指针数组和数组指针

指针数组是指数组元素全为指针的数组;数组指针是一个指针,它指向的是一个数组。

1
2
3
4
5
6
7
// 区分下列哪个是指针数组,哪个是数组指针
int *p1[5];

int (*p2)[5]; //int (*p)[5]就相当于int a[5],a就是数组地址!!! (区别于数组首地址,后面会讲到)

//[ ]优先级大于*;虽然[]和()优先级一样,但结合性是从左到右,故第一个是指针数组,第二个是数组指针。


指针数组的一个用途:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>

int main(){
int i;
char *p[3] = { //由于指针数组存放的都是指针,而数组名就是指针变量。
"让编程改变世界",
"Just do it!",
"一切皆有可能"
};
for (i = 0; i<3; i++){
printf("%s\n",p[i]); // %s是通过字符串首地址输出字符串
}
return 0;
}

关于数组指针还有一个坑,数组指针指向的是一个数组,而我们之前常用int *p = temp;(此处temp是一个已定义的整型数组)来将指针指向数组,但实际上,指针只是指向了数组第一个元素的地址。现在我们要想数组指针指向整个数组,需使用int (*p)[5] = &temp;这里&temp相当于将整个数组看作一个整体来看待的。(必须清楚的是,数组第一个元素的地址跟整个数组的地址是相同的。)举例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
int main()
{
int temp[] = {1,2,3,4,5};
// int (*p)[5] = temp; //输出也可以,因为把数组当作整体给出地址还是数组首地址,但编译器会提醒。
int (*p)[5] = &temp;
int i;

for ( i = 0; i < 5; i++)
{
printf("%d\n",*(*p + i));
// p是一个指向数组(并非指向数组第一个元素)的指针,*p就是取出该指针对应的内容,也就是数组temp,即*p=temp,而temp又是数组第一个元素的地址,可以当作指针,并进行指针运算。
//注意,此时*(*p) = *(temp) = 数组temp的第一个元素.有点嵌套指针的意味了!
//于是,上述输出也可写成: printf("%d\n",(*p)[i]);
}

return 0;
}


小甲鱼课后作业:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int main(){
char *array[5] = {
"FishC",
"Five",
"Star",
"Good",
"WoW"
};
char *(*p)[5] = &array; //定义指向包含5个指针的数组的指针,也就是说这个数组的类型是字符指针类型,故定义相同类型(char *)的数组指针
int i,j;

for (i = 0; i<5; i++){
for (j = 0; *(*(*p+i)+j) != '\0'; j++){
printf("%c ",*(*(*p+i)+j));
}
printf("\n");
}

return 0;
}

此处p实际上是指向包含5个指针的数组,最好写成char (p)[5] = array;

指针与二维数组

观察下述代码:

1
2
3
4
5
6
// 二维数组
int array[4][5] = {0};

printf("%d\n",sizeof(array[0][0]));
printf("array is %p\n",array);
printf("array +1 is %p\n",array+1);

输出结果是:
1
2
array is 0xbfc34320
array +1 is 0xbfc34334

由此得出,二维数组的数组名指向包含5个元素的数组(也就是第一行元素所构成的数组),二维数组名实际上就是数组指针!!!
同时也可明白,array + 1则指向第二行构成的数组,即此处的array +1相当于前述的p.

二维数组名也可赋值给数组指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>

int main(){
int array[2][3] = {
{1,2,3},
{5,8,6}
};
int (*p)[3] = array; // p指向第一行元素构成的数组
int (*n)[3] = array + 1;// n指向第二行元素构成的数组
int i;

for (i = 0; i<3; i++){
printf("%d\t",(*p)[i]);
}
printf("\n");

for (i = 0; i<3; i++){
printf("%d\t",(*n)[i]);
}
printf("\n");

printf("%p\n",p); // 输出第一行元素构成的数组的地址。
printf("%p\n",array);// 输出第一行元素构成的数组的地址。
printf("%p\n",&(*array)); // 输出第一行元素构成的数组的地址。(注意,并非数组的第一个元素,尽管两者数值上相等。)
printf("%p\n",*p); //输出第一行元素构成的数组的第一个元素的地址。

printf("%d\n",**(p+1));// p是第一行元素构成的数组的地址,(p+1)则是第二行元素构成的数组的地址。
//因为二维数组在内存中也是线性存储的,p+1表示指针往后移3*4=12个字节,也就是第二行元素构成的数组的地址。
//即p+1指向第二行元素构成的数组。

return 0;
}

void指针与NULL指针

void指针我们把它称之为通用指针,就是可以指向任意类型的数据。也就是说,任何类型的指针都可以赋值给void指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>

int main(){
int num = 5;
int *p = &num;
char *n = "string";
void *ye;

ye = p;
printf("ye:%p p:%p\n",ye,p);
ye = n;
printf("ye:%p n:%p\n",ye,n);

return 0;
}

于是,又引出几个问题:

  1. void指针如何取值?编译器怎么知道?
    • 所以,强制转换符(强制转换类型 *)又出现了。
  2. 对于 void指针 pp + 1移动多少个字节?
    • 由于编译器不知道其指向的数据类型,故只移动一个字节。
      1
      printf("ye:%d\n",*((int *)ye));  
      NULL指针,即空指针,不指向任何一个地址。
      1
      2
      3
      4
      5
      6
      // 空指针的宏定义
      #define NULL ((void *)0)
      //
      int *p = NULL; //定义空指针,解引用(取值)的话程序会报错。
      //也可以写成 int *p = (void *)0 ;
      int *m; //还未初始化,称为野指针。
      注意,是NULL而不是NUL(在ASCII表中)。
    1. NULL用于指针和对象,表示控制,指向一个不被使用的地址。
    2. NUL(‘\0’)表示字符串的结尾。

小甲鱼S1E24:指针和二维数组第5题出现了(int (*)[3])强制类型转换符。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main()
{
int array[9] = {1, 2, 3, 4, 5, 6, 7, 8, 9};
int (*p)[3] = (int (*)[3])&array;

printf("%d\n", p[2][2]);

return 0;
}

(int (*)[3])&array等号右边强制将 array 这个一位数组重新划分成3*3的二维数组,p等于二维数组。这与前面第6条所说:二维数组名就是数组指针相呼应。小甲鱼的答案有点问题,p并不是指向二维数组,p就是二维数组名,指向二维数组第一行元素构成的数组。

指向指针的指针

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(){
int a = 4;
int *p = &a;
int **pp = &p; //此即指向指针的指针

//验证是否指向指针
printf("p = %p\n*pp = %p",p,*pp);

return 0;
}

那指向指针的指针有啥用呢?观察如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>

int main(){
char *cBooks[] = {
"C primer plus",
"带你学C带你飞",
"C与指针"
};
char **charm;

charm = &cBooks[1];

printf("%s",*charm);

return 0;
}

由此可以看出,指向指针的指针至少有以下两个好处:

  • 避免重复分配内存;
  • 只需对一处进行修改.

指向指针的指针与二维数组

观察如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main(){
int array[3][4] = {
{1,2,5,3},
{5,7,25,36},
{2,3,7,5}
};
int **p = array;
int i,j;

for (i = 0; i<3; i++){
for (j = 0; j<4; j++){
printf("%d",*(*(p+i)+j)); //对应着case one
//printf("%d",*(*(array+i)+j)); //对应case two
}
}

return 0;
}

Case one:

由于p是一个指针,故p+i就是指针移动iint数据类型所占的字节。例如,i = 1,则p+1就是第一行元素构成数组的地址+4,如果再取值*,由于找不到对应的内容(因为无论是整个数组地址还是数组第一个元素的地址,数值上都等于数组第一个元素的地址,且二维数组的行并不一定在内存中是连续存储的。)因此,使用错误的指针类型可能导致对内存的错误访问,从而触发段错误sgementation default

Case two:

array可行,因为array本身可看作数组指针,array+1就是移向下一行。

因此,要想正确输出,需将上述代码改为如下:

1
int (*p)[4] = array;  //利用数组指针

这里值得注意的是:

  1. 因为array实际上是第一行元素的构成的数组的地址,[num]num必须要和该数组元素个数相等。p指向一个4个元素的数组,则p + 1移动sizeof(int) * 4个字节。而如果是*p,此时*p是指向第一行元素的构成的数组的第一个元素,此时*p + 1移动sizeof(int)个字节。
  2. 由上可见,正如第2条最后所说,指针所指向的数据类型决定指针的跨度

小试牛刀:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main(){
char *array[4] = {
"hello",
"How are you?",
"I'm fine, thanks",
"and you?"
};
char* (*p)[4] = &array;
int i;
for (i = 0; i<4; i++){
printf("%s",(*p)[i]);
}
return 0;
}

Ps:B站这一节课有弹幕指出:可以写成(*p)[3][4] = &array,这个其实相当于定义了一个指向二维数组的指针。代码可修改如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>
int main(){
int array[3][4] = {
{1,2,5,3},
{5,7,25,36},
{2,3,7,5}
};
int (*p)[3][4] = &array;
int i,j;
for (i = 0; i<3; i++){
for (j = 0; j<4; j++){
printf("%d\t",*(*(*p+i)+j)); //由于p指向整个二维数组,故*p就是二维数组,指向二维数组第一行元素构成的数组,这样也就弱化为指向数组的指针了,同前述第6节。
}
printf("\n");
}
return 0;
}

常量和指针

常量:

1
520, 'A', ...

常变量:
1
const int a = 5; //这样使得a只能读,不能修改,相当于常量,但不是常量.

定义指向常量的指针(区别于后续所讲常量指针):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const int num = 520;
const int *p = &num; //定义指向常量的指针

//如果尝试修改指向常量的指针指向的值,则会发生错误
*p = 1250;
//error: assignment of read-only location '*p'

//如果尝试修改指向常量的指针的值,这是允许的,相当于该指针不指向常量num.
int cnum = 250;
p = &cnum;
//编译通过。

//此时如果改变p指向的值,则报错:
*p = 1024;
//但是,我们如果修改cnum的值:
cnum = 1024;
//编译成功。

指向常量的指针-总结:

  • 指针可以修改为指向不同的常量
  • 指针可以修改为指向不同的变量
  • 可以通过解引用来读取指针指向的数据
  • 不可以通过解引用修改指针指向的数据

那什么是常量指针呢?

1
2
int num = 520;
int * const p = &num; //定义指向非常量的常量指针

特性:

  • 指针本身值不可改变,指向的值可修改。
  • 典型例子就是数组名

定义指向非常量的常量指针:

1
2
3
4
5
6
7
8
9
10
11
int num = 520;
int cnum = 1024;
int * const p = &num;

//修改指向的值:
*p = 1024;
//编译成功;

//修改本身值:
p = &cnum;
//报错:error: assignment of read-only variable 'p';

如果定义一个指向常量的常量指针:
1
2
3
4
5
6
7
8
9
10
int num = 520;
int cnum = 1024;
const int * const p = &cnum;

//如果此时既修改指针的值,又修改指针所指向的值,则会报两个错:
*p = 20;
p = &num;

// error:error: assignment of read-only location '*p';
// error:error: assignment of read-only variable 'p';

可以看出其特点:

  • 指针本身值不可改变,指向的值也不可修改。

但有趣的一点是,如果你定义了指向常量的常量指针,但接受的地址不是常量,还是可以通过改变该非常量来修改指针所指向的值的。如下:

1
2
3
4
5
6
7
8
int num = 520;
int cnum = 1024;
const int * const p = &num;

num = 111;
printf("*p = %d",*p);

//输出111.

又进一步引出:指向 “指向常量的常量指针” 的指针。
看到名字别害怕,去掉定语,也即指向指针的指针

要记住:一般来说,要指向 “常量指针”,自己也必须是指向常量的指针。
但如果自己不是指向常量的常量指针也可以的,不过会有warning, 后面详细介绍了const的约束对象,理解将会更加深刻。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int num = 520;
int cnum = 1024;
const int * const p = &num;
const int ** pp = &p; //定义指向 “常量指针” 的指针
//上行代码也可以写成(但不推荐):
//int **p = &p;


//验证pp指向p
printf("pp = %p, &p = %p\n",pp,&p);
//验证*pp == p == &num
printf("*pp = %p, p = %p,&num = %p\n",*pp,p,&num);
//验证**pp == *p == num;
printf("**pp = %d, *p = %d, num = %d\n",**pp,p,num);

小甲鱼S1E27:常量和指针, 关于const的约束对象
请问在下边声明中,const 限制的是 q、*q 还是 **q?

1
2
3
4
5
6
7
8
9
10
11
12
#include <stdio.h>

int main(void)
{
const int num = 520;
const int * const p = &num;
const int * const *q = &p;

……

return 0;
}

答: 第一个const限制**q(即指针的两次解引用,可以理解为num,但不是num.因为num如果是int num = 250,那么num还是可以修改的,而**q则是不可以的。),第二个const限制*q(同上述理);

记住一点:const 永远限制紧随着它的标识符。那么,如果想要使用 const 同时限制 q、*q 和 **q,应该怎么做?

const int * const * const p; 或者int const * const * const p; 因为const *p等价于 int const *p; 但一般为了可读性,就不写成int const *p,但int const *p更能体现这句话—-const 永远限制紧随着它的标识符.

于是又有人问了,const int const **p; 限制了啥?

其实只限制了**p.

重点:限制了谁谁就变可读了,不能修改(也就不是可修改的左值了)。

函数

函数初体验

  1. C语言程序编译的时候是从上至下,执行的时候是从main()函数开始的。
  2. void函数不返回任何值,因此不需要加return xxx ;
  3. 养成好习惯:要使用某个函数int func(),先进行声明int func();,千万别丢了分号;.
    函数声明时,既可带上参数名,也可不带。例如:
    1
    2
    3
    int func(int x, int y);
    //or
    int func(int, int);
  4. 如果定义一个与标准库函数重名的函数,会怎样?
    • 重新定义的同名函数会覆盖标准库函数(前提是两者的声明一致,包括返回值和参数类型、个数一致)。

参数和指针

  1. 传值和传址
    观察下列两个代码:
    传值:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <stdio.h>

    void swap(int x, int y);

    void swap(int x, int y){
    int temp;

    temp = x;
    x = y;
    y = temp;
    }

    int main(){
    int x = 3, y = 5;

    printf("交换前: x = %d, y = %d\n",x,y);
    swap(x,y);
    printf("交换后: x = %d, y = %d\n",x,y);

    return 0;
    }

    结果输出:

    交换前: x = 3, y = 5
    交换后: x = 3, y = 5

    传址:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <stdio.h>

    void swap(int *x, int *y);

    void swap(int *x, int *y){
    int temp;

    temp = *x;
    *x = *y;
    *y = temp;
    }

    int main(){
    int x = 3, y = 5;

    printf("交换前: x = %d, y = %d\n",x,y);
    swap(&x,&y);
    printf("交换后: x = %d, y = %d\n",x,y);

    return 0;
    }

    结果输出:

    交换前: x = 3, y = 5
    交换后: x = 5, y = 3

为啥结果会不同呢?
因为函数swap(int x, int y)会给x,y单独在内存中找两段储存空间(可理解为局部变量),存放传进来的值,对原来地址上的值无影响。如果传给函数的本就是地址,则函数内部调用时,就是直接对原来地址上的值进行操作。

  1. 传数组
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <stdio.h>

    void get_array(int b[]); //由于传递数组本质是传递数组第一个元素地址,此处可以不写数组元素个数。

    void get_array(int b[]){
    printf("sizeof b: %d\n",sizeof(b)); //由于传递的是一个地址,也就是指针,取决于操作系统。我的操作系统是64位,故返回8。
    }

    int main(){
    int a[10] = {1,2,3,4,5,6,7,8,9,0};

    printf("sizeof a: %d\n",sizeof(a));
    get_array(a);

    return 0;
    }
  2. 可变参数
    首先需要调用stdarg.h头文件,具体实现原理可见可变参数详解,常见用法如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdarg.h>

    int VarArgFunc(int dwFixedArg, ...){ //以固定参数的地址为起点依次确定各变参的内存起始地址

    va_list pArgs = NULL; //定义va_list类型的指针pArgs,用于存储参数地址

    va_start(pArgs, dwFixedArg); //初始化pArgs指针,使其指向第一个可变参数。该宏第二个参数是变参列表的前一个参数,即最后一个固定参数

    int dwVarArg = va_arg(pArgs, int); //该宏返回变参列表中的当前变参值并使pArgs指向列表中的下个变参。该宏第二个参数是要返回的当前变参类型

    //若函数有多个可变参数,则依次调用va_arg宏获取各个变参

    va_end(pArgs); //将指针pArgs置为无效,结束变参的获取

    /* Code Block using variable arguments */

    }

    举个栗子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>
    #include <stdarg.h>

    void func_arg(int n, ...);
    void func_arg(int n, ...){
    va_list pargs = NULL;
    va_start(pargs,n);
    int i = va_arg(pargs, int);
    printf("%d\n",i);
    }

    int main(){
    int i = 10;
    func_arg(10);//打印固定参数i堆栈上方一个内容(垃圾值)
    func_arg(10,5);//打印固定参数i堆栈上方一个内容(5)
    return 0;
    }

    原理图如下所示:

    小试牛刀:尝试实现sum()函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    #include <stdio.h>
    #include <stdarg.h>

    int sum(int n,...);

    int sum(int n,...){
    int i,sum = 0;
    va_list pArgs = NULL;
    va_start(pArgs,n);
    for (i = 0; i < n; i++){
    sum += va_arg(pArgs,int);
    }
    va_end(pArgs);
    return sum;
    }

    int main(){

    printf("sum = %d\n",sum(3,4,5,6));
    return 0;
    }

    勇攀高峰:尝试自己模拟实现 printf 格式化输出的基本功能,基本要求如下:

    • 输出第一个参数中除了格式化占位符外的所有字符
    • 实现 %d 的格式化输出
    • 实现 %c 的格式化输出
    • 实现 %s 的格式化输出
    • 实现 myprintf 函数返回打印了多少字符
    • 全程仅能使用 putchar 唯一一个标准库函数

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      #include <stdio.h>
      #include <stdarg.h>

      int myprintf(char* str,...);
      int printstr(char* str);
      int printint(int num);

      int printstr(char* str){
      int i = 0;
      int count = 0;

      while (str[i] != '\0'){
      putchar(str[i++]);
      count++;
      }

      return count;
      }

      int printint(int num){
      int dec = 1;
      int temp;
      int count = 0;

      if (num < 0){
      putchar('-');
      count++;
      num = -num;
      }

      temp = num;
      while (temp > 9){
      temp /= 10;
      dec *= 10;
      }

      while (dec >= 1){
      putchar(num/dec + '0');
      count++;
      num %= dec ;
      dec /= 10;
      }

      return count;
      }

      int myprintf(char* str,...){
      int num = 0;
      int i = 0, j = 0;
      char carg;
      char *sarg ;
      int darg;

      va_list pArgs = NULL;
      va_start(pArgs,str);

      while (str[i] != '\0'){
      if (str[i] != '%') {
      putchar(str[i++]);
      num++;
      }
      else {
      switch (str[i+1]){
      case 's':{
      sarg = va_arg(pArgs,char*);
      num += printstr(sarg);
      break;
      }
      case 'c':{
      carg = va_arg(pArgs,int);
      putchar(carg);
      num++;
      break;
      }
      case 'd':{
      darg = va_arg(pArgs,int);
      num += printint(darg);
      break;
      }
      }
      i += 2;
      }
      }
      va_end(pArgs);

      return num;
      }

      int main(){
      int i;

      i = myprintf("Hello %s\n", "FishC");
      myprintf("共打印了%d个字符(包含\\n)\n", i);
      i = myprintf("int: %d, char: %c\n", -520, 'H');
      myprintf("共打印了%d个字符(包含\\n)\n", i);

      return 0;
      }

指针函数与函数指针

  1. 指针函数
    定义:使用指针变量作为函数的返回值的函数,就是指针函数。
    1
    2
    3
    4
    5
    6
    char *pointer(); //写成 char* pointer();可能更好理解,但遵循一般习惯,就这样吧。

    char *pointer(){
    ...;
    return 字符指针;
    }
    需要特别注意的是:不要返回局部变量的指针!!!
    举例如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    #include <stdio.h>

    char *get_word(char c);

    char *get_word(char c){
    switch (c){
    case 'A': return "Apple";
    case 'B': return "Banana";
    case 'C': return "Cat";
    case 'D': return "Dog";
    default: return "None";
    }
    }

    //不要写成下面这样,因为函数一结束,所有变量值会被清理掉,虽然返回了变量的地址,但该地址上啥值也没有。
    // char *get_word(char c){
    // char str1[] = "Apple";
    // char str2[] = "Banana";
    // char str3[] = "Cat";
    // char str4[] = "Dog";
    // char str5[] = "None";
    // switch (c){
    // case 'A': return str1;
    // case 'B': return str2;
    // case 'C': return str3;
    // case 'D': return str4;
    // default: return str5;
    // }
    // }

    int main(){
    char c;

    printf("请输入要查询的字符:");
    scanf("%c",&c);

    printf("%s",get_word(c));

    return 0;
    }
    可能又有人问了,为啥可以直接返回字符串”Apple”…哪些呢?
    参考ChatGPT回答:
  2. 函数指针
    定义:指向函数的指针。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    int square(int num);

    int square(int num){
    return num*num;
    }

    int (*fp)(int); //定义函数指针,此处第一个int取决于所指向的函数的返回值,第二个int取决于函数的输入参数。就跟函数定义时相同即可。

    //初始化函数指针
    fp = square; //也可以写成fp = &square; 但没必要,因为一般来说,函数编译后,函数名就是函数的地址。

    //在主函数中通过函数指针调用函数
    int num = 5;
    printf("%d",(*fp)(num));
    //其实也可以写成 printf("%d",fp(num)); 但为了与函数混淆,还是写成上面那种形式比较好。
    需要注意的就是函数编译后,函数名就是函数指针。
  3. 函数指针作为参数
    举个栗子:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    #include <stdio.h>

    int add(int, int);
    int sub(int, int);
    int calc(int (*fp)(int, int), int,int);


    int add(int num1, int num2){
    return num1 + num2;
    }

    int sub(int num1, int num2){
    return num1 - num2;
    }

    //将函数指针作为参数
    int calc(int (*fp)(int, int), int num1 ,int num2){
    return (*fp)(num1,num2);
    }

    int main(){

    printf("3 + 5 = %d\n",calc(add,3,5));
    printf("3 - 5 = %d\n",calc(sub,3,5));

    return 0;
    }
  4. 函数指针作为返回值
    接着上节,举个栗子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    #include <stdio.h>

    int add(int, int);
    int sub(int, int);
    int calc(int (*fp)(int, int), int,int);
    int (*select(char))(int, int); //定义一个参数为char类型,返回值为函数指针的函数.


    int add(int num1, int num2){
    return num1 + num2;
    }

    int sub(int num1, int num2){
    return num1 - num2;
    }

    //函数指针作为参数
    int calc(int (*fp)(int, int), int num1 ,int num2){
    return (*fp)(num1,num2);
    }

    //函数指针作为返回值
    int (*select(char op))(int num1, int num2){
    switch(op){
    case '+': return add;
    case '-': return sub;
    }
    }

    int main(){
    int num1, num2;
    char op;

    printf("请输入计算表达式:");
    scanf("%d%c%d",&num1,&op,&num2);

    printf("%d + %d = %d\n",num1,num2,calc(select(op),num1,num2));

    return 0;
    }

    从上面,我们可以发现,定义返回值类型为函数指针,参数类型为字符的函数,且所返回的函数指针指向的函数接收两个int类型参数且返回值为int类型的操作为: int (*func(int))(int, int);.
    函数拓展 >> snprintf函数,函数详解-> 传送门.
    举个栗子😊:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <stdio.h>

    int main(){
    char c = 42;
    char array[10];
    //将整数42以16进制数的形式写入字符数组
    //42(10) == 2a(16)
    snprintf(array,3,"%02x",c);
    //打印字符串
    printf("%s",array); // 输出结果为2a,从而印证上述说法.

    return 0;
    }

局部变量与全局变量

  1. 局部变量
    局部变量(Local Variables)是指在特定范围内声明和使用的变量。这个特定的范围通常是在一个函数或者一个代码块内部。
    特定范围中一个函数的情况很常见,对于代码块内部,下面举一例进行说明:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <stdio.h> 

    int main(){
    int i = 520;

    printf("before, i = %d\n",i);

    for (int i = 0; i < 10; i++){
    printf("%d\n",i);
    }

    printf("after, i = %d\n",i);

    return 0;
    }

    for循环内部的i只作用于该循环,且循环内部调用的i只能是int i = 0这里定义的。在for循环外面调用的i就是int i = 520;这里定义的!!!
    如下图所示:
    循环内局部变量

    不过要注意的是,C99才允许上述在for语句中定义变量。因为C99允许在使用时才定义变量,而不是一开始在最上面定义变量。

  2. 全局变量
    在函数里面定义的,我们叫局部变量;在函数外部定义的,我们叫外部变量,也叫做全局变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    #include <stdio.h>

    void a();
    void b();

    int count = 0;

    void a(){
    count++;
    }

    void b(){
    count++;
    }

    int main(){
    a();
    b();

    printf("count = %d",count);

    return 0;
    }

    注意:

    • 如果不对全局变量进行初始化,那么它会自动初始化为0
    • 如果在函数的内部存在一个与全局变量同名的局部变量,编译器并不会报错,而是在函数中屏蔽全局变量(也就是说在这个函数中,全局变量不起作用)。

    举例如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <stdio.h>

    int a, b = 520;

    void func();

    void func(){
    a = 880;
    int b = 120;

    printf("a + b = %d\n",a+b);
    }

    int main(){
    printf("a + b = %d\n",a+b);
    func();
    printf("a + b = %d\n",a+b);

    return 0;
    }

    运行结果:

    1
    2
    3
    a + b = 520
    a + b = 1000
    a + b = 1400

    一般来说编译器不允许先使用变量再定义,否则会报错;
    但是如果我们使用关键字extern,则是可以的。该关键字的功能就是告诉编译器,我是定义了这个变量的,只不过在后面;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <stdio.h>

    void func();

    void func(){
    extern int count;
    count++;
    }

    int count = 0;

    int main(){
    func();
    printf("count = %d\n",count);

    return 0;
    }

作用域与链接属性

作用域

作用域包括以下几种类型:

  • 代码块作用域
  • 文件作用域
  • 原型作用域
  • 函数作用域

在代码块中定义的变量,具有代码块作用域。作用范围是从变量定义的位置开始,到标志该代码块结束的右大括号(})处。
另外,尽管函数的形式参数不在大括号内定义,但其同样具有代码块作用域,隶属于包含函数体的代码块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(){
int i = 100;
{
int i = 120;
{
int i = 130;
printf("%d\n",i);
}
printf("%d\n",i);

}
printf("%d\n",i);

return 0;
}

运行结果:
1
2
3
130
120
100

任何在代码块之外声明的标识符都具有文件作用域,作用范围是从它们声明位置开始,到文件的结尾处结束。另外,函数名也具有文件作用域,因为函数名本身也在代码块之外。

定义了一定是声明了!!!典型的就是全局变量的定义,故作用范围是从定义的位置开始,到文件结尾处结束!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>

void func(void); //函数声明

int main(){
extern int count; //全局变量声明。当然也可以直接定义,例如在main函数前面写 int count = 0;
func();
count++;
printf("In main, count = %d\n",count);
return 0;
}

int count = 0;

void func(void){
count++;
printf("In func, count = %d\n",count);
}

运行结果是:

1
2
In func, count = 1
In main, count = 2

很显然,如果没有声明(void func()extern int count),那么编译器也不知道后面会有func和count,就报个错先!
进一步,可以看出,具有文件作用域的标识符包括函数名和全局变量

原型作用域(prototype scope)只适用于那些在函数原型中声明的参数名。我们知道,函数在声明的时候可以不写参数的名字(但参数类型是必须要写上的),其实尝试一下还可以发现,函数原型的参数名还可以随便写一个名字,不必与函数定义时的形式参数相匹配(当然,这样做毫无意义)。之所以允许这么做,是因为原型作用域起了作用。
原型作用域

1
2
3
4
5
6
7
// 函数原型(亦称为函数声明)
int add(int a, int b);

// 函数定义
int add(int a, int b) {
return a + b;
}

函数作用域只适用于语句标签(而语句标签用于goto语句)。使用规则:一个函数的所有语句标签必须唯一。
goto语句的作用域是一个函数的大小,而不是一个代码块。

链接属性

啥是链接?可能刚开始比较疑惑,那就看看C语言源代码是如何编程可执行文件(.exe)的吧!—>传送门:[]
链接属性分类:

  • external(外部属性):多个文件中声明的同名标识符表示同一个实体。
  • internal(内部属性):单个文件中声明的同名标识符表示同一个实体。
  • none(空链接属性):声明的同名标识符中被当作不同独立的实体。比如,函数的局部变量,因为它们被当作独立不同的实体,所以不同函数间同名的局部变量并不会发生冲突。

特点:

  • 只有具备文件作用域的标识符才能拥有external或internal的链接属性,其他作用域的标识符都是none属性。
  • 默认情况下,具备文件作用域的标识符拥有external属性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//a.c 
extern int count;
void a(){
count++;
}

//b.c
extern int count;
void b(){
count++;
}

//c.c
extern int count;
void c(){
count++;
}

//test.c
#include <stdio.h>

void a(void);
void b(void);
void c(void);

int count = 0;

int main(void){
a();
b();
c();
b();

printf("count = %d\n",count);

return 0;
}

使用static关键字对链接属性的修改:

  1. 使用static关键字修改链接属性,只对具有文件作用域的标识符生效(对于拥有其他作用域的标识符是另一种功能)。(当然只是对链接属性进行了修改,作用域还是没变!!!)
  2. 链接属性只能修改一次,也就是说,一旦将标识符的链接属性变为internal,就无法再变回external了。

这样做的好处就是保护全局变量,以免在其他文件中被修改!

本节作用域与链接属性是从空间角度分析的,下面将从时间角度作为切入点!!!

生存期与存储类型

生存期

C语言拥有两种生存期:

  • 静态存储期(static storage duration)
  • 自动存储期(automatic storage duration)

具有文件作用域的变量属于静态存储期,函数也属于静态存储期。属于静态存储期的变量在程序执行期间将一直占据存储空间,直到程序关闭才释放

具有代码块作用域的变量一般情况下属于自动存储期。属于自动存储期的变量在代码块结束时将自动释放存储空间

存储类型

C语言提供了五种不同的存储类型:

  • atuo(default)
  • register
  • 静态存储变量(static)
  • static 和 extern
  • typedef

默认适用对象:函数中的形参、局部变量及复合语句中定义的局部变量等;
特性:拥有代码块作用域、自动存储期和空连接属性(None);
在代码块中声明的变量默认的存储类型就是auto,不过auto可以省略,平常我们写的时候就省略了。
不过当强调局部变量屏蔽全局变量这一做法时,可以在局部变量前加上auto。

声明:register int i;…
特性

  • 代码块作用域、自动存储期和空链接属性;
  • 不能通过取址运算符(&)获得该变量的地址!!!

将一个变量声明为寄存器变量,那么该变量就有可能被存放于位于CPU的寄存器。为啥说有可能呢?因为CPU的寄存器空间十分有限,所以编译器并不会让所有声明为register的变量都放到寄存器中。
事实上,有可能所有的register关键字都被忽略,因为编译器有自己的一套优化方法,会权衡哪些才是最常用的变量。在编译器看来,它比你更了解程序。而那些被忽略的register变量,它们会变成普通的自动变量。

首先可能有人会好奇,为啥会叫静态局部变量呢?
这就对了,因为static作用于全局变量时,是修改其链接属性。那么static作用于局部变量会发生什么奇妙的事情呢?
static作用于局部变量会使局部变量的生存期由自动存储期变为静态储存期!由此可得其特性:
特性:空链接属性、代码块作用域、静态存储期。
举例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
void func(void);

void func(void) {
static int count = 0;
printf("count = %d\n",count);
count++;
}

void main(void){
int i ;
for (i = 0; i < 10; i++){
func();
}
}

输出结果为:
1
2
3
4
0
1
...
9

此处所指static不同于前述静态局部变量所讲的static。这里所讲static和extern的作用域是文件作用域,static关键字使得默认具有external链接属性的标识符变成internal链接属性,而extern关键字告诉编译器这个变量或函数在别的地方已经定义过了,先去别的地方找找,不要急着报错。

该类型将在结构体章节详细阐述!!!

递归

切记:递归一定要有结束条件!
基操:实现阶乘

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
int recursion(int);

int recursion(int n){
if (n == 0){
return 1;
}
return recursion(n-1)*n;
}

int main(void){
int n;

printf("please input number:");
scanf("%d",&n);

printf("%d! = %d",n,recursion(n));

return 0;
}

小试牛刀:

汉诺塔
实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
int func(int, char, char, char);

int func(int n,char x, char y, char z){ //表示的意义为:从x移到z位置,借助y;
if (n == 1){
printf("%c --> %c\n", x, z);
}
else {
func(n-1, x, z, y);
printf("%c --> %c\n", x, z);
func(n-1, y, x, z);
}
}

void main(void){
int n;
char x = 'x',y = 'y',z = 'z';

printf("please input story:");
scanf("%d",&n);

func(n,x,y,z);
}

快速排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <stdio.h>

void quick_sort(int *p, int start, int end){
int i = start, j = end;
int judge = p[(start+end)/2];
int temp;

while (1){
for (; p[i] < judge; i++){
}
for (; p[j] > judge; j--){
}
if (i > j){
break;
}
temp = p[i];
p[i] = p[j];
p[j] = temp;
i++; //特别要注意,比较完之后要向中间靠拢,否则遇见p[i] == p[j]的情况就会陷入死循环!
j--;
}

//最后i和j一定是相邻的!因为i走过的必定是满足p[i] < judge,也就不可能满足p[j] > judge了;同理,j走过的必定不可能满足i,故i,j最后会相邻,且i > j!

if (j > start){
quick_sort(p, start, j);
}
if (i < end){
quick_sort(p, i, end);
}

}

void main(void){
int array[] = {388,458,253,245,356,356,122,223,245};
int i, length;

length = sizeof(array)/sizeof(array[0]) - 1;

quick_sort(array, 0, length);

printf("the sorted result is\n");
for (i = 0; i <= length; i++){
printf("%d\n",array[i]);
}
}

动态内存管理

在之前所学中,变量一经定义,其内存大小就不能再更改了!那么有什么办法能让C语言更灵活地管理内存资源呢?
答案是有的,需要借助几个库函数,这几个库函数在stdlib.h这个头文件中!

malloc函数

函数原型:

1
2
3
#include <stdio.h>
...
void *malloc(size_t size);

malloc函数向系统申请分配size个字节的内存空间,并返回一个指向这块空间的指针。不过要注意,申请的这块空间并没有被“清理”(初始化为0),所以它上面的数据是随机的(就与局部变量一样)。

  • 如果函数调用成功,会返回一个指向申请的内存空间的指针,由于返回类型是void指针(void*),所以它可以被转换成任何类型的数据:
  • 如果函数调用失败,返回值是NULL。
  • 另外,如果size参数设置为O,返回值也可能是NULL,但这并不意味着函数调用失败。
1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>

void main(void){
int *ptr;

ptr = (int *)malloc(sizeof(int));

printf("please input number:");
scanf("%d",ptr);

printf("Num is %d\n",*ptr);
}

free函数

函数原型:

1
2
...
void free(void *ptr);

由于malloc函数申请的空间位于内存的堆上,如果不主动释放它,那么它会一直存在直到程序结束!
所以以后写程序,mallocfree要成对!
故上面程序应该为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>

void main(void){
int *ptr;

ptr = (int *)malloc(sizeof(int));

printf("please input number:");
scanf("%d",ptr);

printf("Num is %d\n",*ptr);
free(ptr); //释放内存
}

内存泄漏

内存泄漏指的是在程序运行过程中,动态分配的内存空间没有被正确释放的情况。导致内存泄漏主要有以下两种情况:

  1. 隐式内存泄漏(即用完内存块没有及时使用free函数释放掉);
  2. 丢失内存地址;

丢失内存地址就是说把原本指向动态申请的内存地址的指针指向别的地方了。例如:

1
2
3
4
5
6
7
...
int num = 520;
int *ptr;

ptr = (int *)malloc(sizeof(int));

ptr = &num; //丢失内存地址

申请任意尺寸的内存空间

malloc不仅可以申请基本类型数据的空间,还可以申请一块任意的内存空间。对于后者,由于申请得到的空间是连续的,通常采用数组来进行索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdlib.h>

void main(void){
int *ptr;
int num = 5;

ptr = (int *)malloc(num * sizeof(int));

for (int i = 0; i < num; i++){
printf("请输入一个整数:");
scanf("%d",&ptr[i]);
}

for (int i = 0; i < num; i++){
printf("ptr[%d] == %d\n", i, ptr[i]);
}

free(ptr);
}

由于malloc并不会初始化申请的内存空间,所以需要自己进行初始化。当然可以写个循环来做这件事,但不建议这么做,标准库提供了更加高效的函数:memset。以mem开头的函数被编入字符串标准库,函数的声明包含在string.h这个头文件中.—>memset函数传送门.

除了memset函数之外,类似的还有memcpy,memmove,memcmp等等,可见小甲鱼函数快查.—>传送门

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main(void){
int *ptr;
int num = 5;

ptr = (int *)malloc(num * sizeof(int));

memset(ptr, 0, num*sizeof(int));

for (int i = 0; i < num; i++){
printf("ptr[%d] == %d\n",i,ptr[i]);
}

free(ptr);
}

细心的同学可能发现了,这样每次写完malloc,还要再写memset,略显繁琐,于是C语言提供了calloc函数一步实现上述功能!

calloc函数

函数原型:

1
2
...
void *calloc(size_t nmemb, size_t size);

calloc函数在内存中动态地申请nmemb个长度为size的连续内存空间(即申请的总空间尺寸为nmemb*size),这些内存空间全部被初始化为0。

如果函数调用成功,会返回一个指向申请的内存空间的指针,由于返回类型是void指针(void*),所以它可以被转换成任何类型的数据:如果函数调用失败,返回值是NULL。如果nmemb或size参数设置为O,返回值也可能是NULL,但这并不意味着函数调用失败。

calloc函数与malloc函数的一个重要区别是:calloc函数在申请完内存后,自动初始化该内存空间为0,而malloc函数不进行初始化操作,里边数据是随机的。因此,下面两种写法是等价的:

1
2
3
4
int *ptr = (int *)calloc(8, sizeof(int));
//
int *ptr = (int *)malloc(8*sizeof(int));
memset(ptr, 0, 8*sizeof(int));

realloc函数

在现实操作中,我们会经常碰到内存空间不足的问题,需要扩展,此时可以借mallocmemccpy进行扩充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main(void){
int *ptr1;
int *ptr2;
int num = 5;

ptr1 = (int *)malloc(num * sizeof(int));

for (int i = 0; i < num; i++){
printf("请输入一个整数:");
scanf("%d",&ptr1[i]);
}

ptr2 = (int *)malloc((num*2)*sizeof(int));
memcpy(ptr2, ptr1, num*sizeof(int));

ptr2[num] = 5201314;

printf("ptr2[%d] == %d\n", num, ptr2[num]);

for (int i = 0; i < num; i++){
printf("ptr2[%d] == %d\n",i, ptr2[i]);
}

free(ptr1);
free(ptr2);
}

这样的操作比较繁琐,还好C语言有相应的库函数,没错,就是realloc!
函数原型:

1
2
...
void *realloc(void *ptr, size_t size);

不过需要注意以下几点:

  • realloc函数将ptr指向的内存空间大小修改为size字节。
  • 如果新分配的内存空间比原来的大,则旧内存块的数据不会发生改变:如果新分配的内存空间比原来的小,则可能导致数据丢失,请慎用。
  • 该函数将移动内存空间的数据并返回新的指针。
  • 如果ptr参数为NULL,那么调用该函数就相当于调用malloc(size)。
  • 如果size参数为0,并且ptr参数不为NULL,那么调用该函数就相当于调用free(ptr)。
  • 除非ptr参数为NULL,否则,ptr的值必须由先前调用malloc、calloc或realloc函数返回。

编写一个程序,不断地接收用户输入的整数,直到用户输入-1表示输入结束,将所有数据打印出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main(void){
int *ptr = NULL; //此处必须初始化为NULL,因为realloc()参数中要求了!如前述注意事项最后一点!
int count = 0;
int num ;

do {
printf("please input number(-1: exit):");
scanf("%d",&num);
count++;
ptr = (int *)realloc(ptr,count * sizeof(int));
ptr[count-1] = num;
} while (num != -1);

for (int i = 0; i < count; i++){
printf("ptr[%d] == %d\n", i, ptr[i]);
}

free(ptr);
}

无论是malloc、calloc还是realloc函数,都需要搭配free函数释放内存!

文件操作

顺序读写文件

本节主要练习那些文件操作函数,见传送门->C语言函数索引
常用的,例如:

  • fopen();
  • fputc()/putc(); (区别于putchar,看着很像)
  • fgetc()/getc(); (区别于getchar())
  • fputs();
  • fgets();

这一节还有几个比较有趣的拓展阅读: