0%

C 基础语法

logo

一、基本数据类型的取值范围

C-基础语法

符号位

存放 signed 类型的存储单元中,左边第一位表示符号位。如果该位为 0,表示该整数是一个正数;如果该位为 1,表示该整数是一个负数。

一个 32 位的整型变量,除去左边第一位符号位,剩下表示值的只有 31 个比特位。

补码

  • 正数的补码是该数的二进制形式。
  • 负数的补码需要通过以下几步获得:
    1. 先取得该数的绝对值的二进制形式
    2. 再将第 1 步的值按位取反
    3. 最后将第 2 步的值加 1

C-基础语法_补码1
C-基础语法_补码2

使用补码的好处

使用补码的好处

二、数组与指针

定义指针变量

1
2
3
类型名 *指针变量名
char *pa; // 定义一个指向字符型的指针变量
int *pb; //定义一个指向整型的指针变量

取地址运算符和取值运算符

如果需要获取某个变量的地址,可以使用取地址运算符 &:

1
2
char *pa = &a;
int *pb = &f;

如果需要访问指针变量指向的数据,可以使用取值运算符 *:

1
printf("%c, %d\n", *pa, *pb);

数组名的真实身份

数组名其实是数组第一个元素的地址!

指向数组的指针

如果用一个指针指向数组,应该怎么做呢?

1
2
3
4
a = {0, 1, 2}
char *p;
p = a; // 语句 1
p = &a[0]; // 语句 2

指针的运算

  • 当指针指向数组元素的时候,我们可以对指针变量进行加减运算,这样做的意义相当于指向距离指针所在位置向前或向后第 n 个元素。
  • 对比标准的下标法访问数组元素,这种使用指针进行间接访问的方法叫做指针法。
  • 需要郑重强调的是:p+1 并不是简单地将地址加 1 而是指向数组的下一个元素。

C-基础语法_指针的运算

指针++的坑和左值

C-基础语法_指针++的坑和左值

指针和数组的区别

结论:数组名只是一个地址,而指针是一个左值。

指针数组和数组指针

C-基础语法_指针数组和数组指针

指针数组

C-基础语法_指针数组

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

int main()
{
char *p1[5] = {
"今天天气不错",
"这是第二句",
"无论未来什么结局",
"从来都不需要回忆"};

for (int i = 0; i < 5; i++)
{
// 这里不能加 *,p1[i] 返回的是字符元组中一个元素的地址,就是一句话,而加上 *p1 就会返回一句话中的一个字
printf("%c\n", p1[i]);
}
}

数组指针

C-基础语法_数组指针

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

int main()
{
int temp[5] = {1, 2, 3, 4, 5};
// int(*p)[5] = temp; //错误 数组第一个元素的地址,就等于数组名和数组的地址,这样定义代表指针指向第一个元素,而不是指向数组

// int *p = temp; //错误 这样使用其实是指向一个变量,并不是指向一个数组的指针
int(*p)[5] = &temp; //正解 而数组指针才是名正言顺的指向数组的指针,所以这里要的并不是变量的地址,而是一个整体的数组地址 &temp

for (int i = 0; i < 5; i++)
{
printf("%d\n", *(*p + i)); // *p 表示 temp 数组的地址,+i 是获取第 2、3、4...号元素地址
// 再用 * 解开就是取值
}
}

array 表示的是什么?

C-基础语法_array表示的是什么1
C-基础语法_array表示的是什么2
C-基础语法_array表示的是什么3
C-基础语法_array表示的是什么4

void 指针和 NULL 指针

void 指针

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

NULL 指针

1
# define NULL ( ( void * ) 0 ) 

NULL 不是 NUL:NULL 用于指针和对象,表示指向一个不被使用的地址; 而 \0 表示字符串的结尾。

养成良好的编程习惯
当你还不清楚要将指针初始化为什么地址时,请将它初始化 NUL;在对指针进行解引用时,先检查该指针是否为 NULL。这种策略可以为你今后编写大型程序节省大量的调试时间。

void 指针示例:

C-基础语法_void-指针示例

指向指针的指针

C-基础语法_指向指针的指针

指针数组和指向指针的指针:

  • 至少有两个好处:

    • 一避免重复分配内存
    • 一只需要进行一处修改
  • 代码的灵活性和安全性都有了显著地提高!

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
#include <stdio.h>

int main()
{
char *set[4] = {
"大陆向前",
"大陆向后",
"海洋逆行",
"海洋东去"
};

char **big[2]; // 定义指向指针的 指针数组
char **water[2]; // 定义指向指针的 指针数组

big[0] = &set[0]; // 数组内放的是地址
big[1] = &set[1];
water[0] = &set[2];
water[1] = &set[3];

printf("%s\n", *big[0]); // big[0] 取出来的是 set[0] 的地址,再解引用就是值了
printf("%s\n", *big[1]);
printf("%s\n", *water[0]);
printf("%s\n", *water[1]);
}

数组指针和二维数组

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[3][4] = {
0, 1, 2, 3,
4, 5, 6, 7,
8, 9, 10, 11};

// int **p = array; // 如果是指向二位数组的话,这样用指向指针的指针的错误的
int (*p)[4] = array; // 正确用法 1
// 要着重考虑跨度的问题
// p 的跨度是 4 * sizeof(int)
// p 是指向数组 array 的指针,array 是数组第 1 行的第 1 个元素的地址
// 那么 p + 1 就等于把指针移动了 4 * sizeof(int),就等于指向了 array 的第 2 行第 1 个

// int(*p)[3][4] = &array; // 正确用法 2

for (int i = 0; i < 3; i++)
{
for (int j = 0; j < 4; j++)
{

printf("%2d", *(*(p + i) + j)); // 解法 1
// 等同于 printf("%2d", p[i][j]);

// printf("%2d", *(*(*p + i) + j)); // 解法 2
// 等同于 printf("%2d", (*p)[i][j]);
}
printf("\n");
}
}

常量

  • 常量应该是这样:520‘a’3.14
  • 或者这样:
    • # define PRICE 520
    • # defineA 'a'
    • # define PI 3.14
  • 还可以使用 const 关键字修饰:
    • const int price = 520;
    • const char a = 'a';
    • const float pi 3.14;

指向常量的指针

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

常量指针

  • 指向非常量的常量指针:
    • 指针自身不可以被修改;
    • 指针指向的值可以被修改;
  • 指向常量的常量指针:
    • 指针自身不可以被修改;
    • 指针指向的值也不可以被修改。

指针函数

使用指针变量作为函数的返回值,就是指针函数。

C-基础语法_指针函数

三、零散的点

可变参数

C-基础语法_可变参数

变量 i 的作用域

C-基础语法_变量i的作用域

全局变量

在函数里边定义的,我们叫局部变量;在函数外边定义的,我们叫外部变量,也叫全局变量。

有时候,我们可能需要在多个函数中使用共同的一个变量,那么就会用到全局变量。因为全局变量可以被本程序中其他函数所共用的。

如果不对全局变量进行初始化,那么它会自动初始化为 0。

如果在函数的内部存在一个与全局变量同名的局部变量,编译器并不会报错,而是再函数中屏蔽全局变量(也就是说在这个函数中,全局变量不起作用)。

不要大量的使用全局变量:

  • 使用全局变量会使你的程序占用更多的内存,因为全局变量从被定义时候开始,直到程序退出才被释放;
  • 污染命名空间,虽然局部变量会屏蔽全变量,但这样一来也会降低程序的可读性,人们往往很难一下子判断出每个变量的含义和作用范围;
  • 提高了程序的耦合性,牵一发而动全身,时间久了,代码长了,你都不知道全局变量被哪些函数修改过。

定义和声明

  • 当一个变量被定义的时候,编译器为变量申请内存空间并填充一些值;
  • 当一个变量被声明的时候,编译器就知道该变量被定义在其他地方;
  • 声明是通知编译器该变量名及相关的类型已存在,不需要再为此申请内存空间;
  • 局部变量既是定义又是声明;
  • 定义只能来一次,否则就叫做重复定义某个同名变量;而声明可以有很多次。

extern 关键字

extern 关键字告诉编译器:这个变量我在后边定义了,你先别急着报错!

链接属性

C-基础语法_链接属性

  • external(外部的):
    • 多个文件中声明的同名标识符表示同一个实体;
  • internal(内部的):
    • 单个文件中声明的同名标识符表示同一个实体;
  • none(无):
    • 声明的同名标识符被当作独立不同的实体;

只有具备文件作用域的标识符才能拥有 external 或 internal 的链接属性,其他作用域的标识符都是none 属性。

默认情况下,具备文件作用域的标识符拥有 external 属性。也就是说该标识符允许跨文件访问。对于 external 属性的标识符,无论在不同文件中声明多少次,表示的都是同一个实体。

生存期

C 语言的变量拥有两种生存期:

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

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

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

四、存储类型

存储类型其实是指存储变量值的内存类型,C 语言提供了 5 中不同的存储类型:

  • auto
  • register
  • static
  • extern
  • typedef

自动变量 (auto)

在代码块中声明的变量默认的存储类型就是自动变量,使用关键字 auto 来描述。

由于这是默认的存储类型,所以不写 auto 是完全没问题的。

寄存器变量 (register)

将一个变量声明为寄存器变量,那么该变量就有可能被存放于 CPU 的寄存器中。

寄存器变量和自动变量在很多方面的是一样的,它们都拥有代码块作用域,自动存储期和空连接属性。

不过这里有一点需要注意的是:当你将变量声明为寄存器变量,那么你就没办法通过取址运算符获得该变量的地址。

static 和 extern

作用于文件作用域的 static 和 extern。

static 关键宇使得默认具有 external 链接属性的标识符变成 internal 链接属性。

而 extern 关键字是用于告诉编译器这个变量或函数在别的地方已经定义过了,先去别的地方找找,不要急着报错。

五、内存空间管理

更灵活的内存管理方式:

  • malloc:申请动态内存空间;
  • free:释放动态内存空间;
  • calloc:申请并初始化一系列内存空间;
  • realloc:重新分配内存空间。

malloc

函数原型:void *malloc(size_ size);

malloc 函数向系统申请分配 size 个字节的内存空间,并返回一个指向这块空间的指针。

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

free

函数原型:void free(void *ptr);

free 函数释放 ptr 参数指向的内存空间。该内存空间必须是由 malloc、calloc 或 realloc 函数申请的。否则,该函数将导致未定义行为。如果 ptr 参数是 nLL,则不执行任何操作。注意:该函数并不会修改 ptr 参数的值,所以调用后它仍然指向原来的地方 (变为非法空间)。

malloc 和 free 的基本使用方法

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>

int main()
{
int *ptr;
ptr = (int *)malloc(sizeof(int)); // 返回的是申请的内存地址的指针

if (ptr == NULL)
{
printf("create memory error");
exit(1);
}

printf("please input a num: ");
scanf("%d", ptr); // 因为返回的是指针,所以不用加 &

printf("you input nu is: %d\n", *ptr);

free(ptr); // 申请了内存一定记得释放

return 0;
}

malloc 还可以申请一块任意尺寸的内存空间:

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
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int num, i;
int *ptr = NULL;

printf("请输入个数:");
scanf("%d", &num);

ptr = (int *)malloc(num * sizeof(int)); // 申请指定大小的内存空间

for (i = 0; i < num; i++)
{
printf("请输入第 %d 个数:", i + 1);
scanf("%d", &ptr[i]); // 赋值
}

printf("你输入的是:");
for (i = 0; i < num; i++)
{
printf("%d\n", ptr[i]);
}

free(ptr); // 不用后记得释放

return 0;
}

初始化内存空间

以 mem 开头的函数被编入字符串标准库,函数的声明包含在 string.h 这个头文件中:

  • memset:使用一个常量字节填充内存空间;
  • memcpy:拷贝内存空间;
  • memmove:拷贝内存空间;
  • memcmp:比较内存空间;
  • memchr:在内存空间中搜索一个字符。

申请内存空间后使用 memset 进行初始化。

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>


#define N 10

int main(void)
{
int i;
int *ptr = NULL;

ptr = (int *)malloc(N * sizeof(int));
if (ptr == NULL)
{
printf("create memory error");
exit(1);
}

memset(ptr, 0, N * sizeof(int)); // 初始化内存空间,用 0 填充

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

free(ptr); // 不用后记得释放

return 0;
}

也可以使用 calloc 将两步操作合并执行。

C-基础语法_内存管理

calloc

函数原型:void*calloc(size_t nmemb, size_t size);

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

calloc 函数与 malloc 函数的一个重要区别是:

  • calloc 函数在申请完内存后,自动初始化该内存空间为零
  • malloc 函数不进行初始化操作,里边数据是随机的。

内存拷贝

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>

int main(void)
{
int *ptr1 = NULL;
int *ptr2 = NULL;

// 第一次申请内存
ptr1 = (int *)malloc(10 * sizeof(int));

// 之前申请的内存空间不够用了,再申请一块大一些的
ptr2 = (int *)malloc(20 * sizeof(int));

// 将 ptr1 中的数据 copy 到 ptr2 中
memcpy(ptr2, ptr1, 10);

// 把 ptr1 释放
free(ptr1);

return 0;
}

也可以使用 reallor 将两步操作合并执行。

reallor

函数原型: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
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void)
{
int i, num;
int count = 0;

// 注意:这里必须初始化为 NULL。因为当 ptr 为 NULL 时,调用 realloc 就相当于调用 malloc(size)
int *ptr = NULL;

do
{
/* code */
printf("请输入一个整数(输入 -1 代表结束)");
scanf("%d", &num);
count++;

// 第一次调用时,因为 ptr 为 NULL,等于调用 malloc(size)
ptr = (int *)realloc(ptr, count * sizeof(int));
if (ptr == NULL)
{
exit(1);
}

ptr[count - 1] = num; // 保存到内存中
} while (num != -1);

printf("刚才输入的数字有:");
for (i = 0; i < count; i++)
{
printf("%d ", ptr[i]);
}

return 0;
}

内存泄露

导致内存泄漏主要有两种情况:

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

隐式内存泄露

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

int main(void)
{
while (1)
{
int *p = malloc(10240);
}

return 0;
}

丢失内存块地址

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

int main(void)
{
int num = 0;
int *ptr = malloc(10240);

ptr = &num;
free(ptr);

return 0;
}

C 语言的内存分布规律

C-基础语法_内存布局规律1.png

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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int global_uninit_var;
int global_init_var1 = 520;
int global_init_var2 = 880;

void func(void);

void func(void)
{
;
}

int main(void)
{

int local_var1;
int local_var2;

static int static_uninit_var;
static int static_init_var = 456;

char *str1 = "monster";
char *str2 = ".com";

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

printf("addr of func -> %p\n", func);
printf("addr of str1-> %p\n", str1);
printf("addr of str2-> %p\n", str2);
printf("addr of global_init_var1 -> %p\n", &global_init_var1);
printf("addr of global_init_var2 -> %p\n", &global_init_var2);
printf("addr of static_init_var -> %p\n", &static_init_var);
printf("addr of static_uninit_var -> %p\n", &static_uninit_var);
printf("addr of global_uninit_var -> %p\n", &global_uninit_var);
printf("addr of func -> %p\n", malloc_var);
printf("addr of local_var1 -> %p\n", &local_var1);
printf("addr of local_var2 -> %p\n", &local_var2);

return 0;
}

C-基础语法_内存布局规律2.png

代码段

代码段(Text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定, 并且内存区域通常属于只读。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。

数据段

数据段(Initialized data segment)通常用来存放已经初始化的全局变量和局部静态变量。

BBS 段(Bss segment/Uninitialized data segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS 是英文 Block Started by Symbol 的简称,这个区段中的数据在程序运行前将被自动初始化为数字 0。

堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩展或缩小。当进程调用 ma1loc 等函数分配内存时,新分配的内存就被动态添加到堆上;当利用 free 等函数释放内存时,被释放的内存从堆中被剔除。

大家平时可能经常听到堆栈这个词,一般指的就是这个栈。栈是函数执行的内存区域,通常和堆共享同一片区域。

堆和栈的区别

  • 申请方式:

    • 堆由程序员手动申请;
    • 栈由系统自动分配。
  • 释放方式:

    • 堆由程序员手动释放;
    • 栈由系统自动释放。
  • 生存周期:

    • 堆的生存周期由动态申请到程序员主动释放为止,不同函数之间均可自由访问;
    • 栈的生存周期由函数调用开始到函数返回时结束,函数之间的局部变量不能互相访问。
  • 发展方向:

    • 堆和其它区段一样,都是从低地址向高地址发展;
    • 栈则相反,是由高地址向低地址发展。

六、宏定义

C-基础语法_宏定义

不带参数的宏定义

1
#define PI 3.14
  • 为了和普通的变量进行区分,宏的名字通常我们约定是全部由大写字母组成;
  • 宏定义只是简单地进行替换,并且由于预处理是在编译之前进行,而编译工作的任务之一就是语法检查,所以编译器不会对宏定义进行语法检查;
  • 宏定义不是说明或语句,在末尾不必加分号;
  • 宏定义的作用域是从定义的位置开始到整个程序结束;
  • 可以用 #undef 来终止宏定义的作用域宏定义允许嵌套。

带参数的宏定义

1
#define MAX(x, y) (((x)>(y)) ? (x) : (y))

下面这样写是错误的!

1
#define MAX (x, y) (((x) > (y)) ? (x) : (y))

七、结构体

结构体声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct 结构体名称
{
结构体成员1;
结构体成员2;
结构体成员3;
......
};

struct Book
{
char title[128];
char author[40];
float price;
unsigned int date;
char publisher[40];
}

定义结构体类型变量

1
struct 结构体名称 结构体变量名

结构体在声明时并不会占用内存,当被真正定义时才会申请内存空间。

访问结构体变量

要访问结构体成员,我们需要引入一个新的运算符 – 点号 . 运算符。

比如 book.title 就是引用 book 结构体 title 的成员,它是一个字符数组;而 book.priceprice 则是引用 book 结构体的成员,它是一个浮点型的变量。

初始化结构体变量

初始化一个变量和数组:

1
2
int a = 520;
int array[5]= {1,2,3,4,5};

初始化一个结构体变量:

1
2
3
4
5
6
7
struct Book book = {
"《带你学C带你飞》",
"小甲鱼",
48.8,
20171111,
"清华大学出版社"
};

初始结构体的指定成员值

其语法和数组指定初始化元素类似,不过结构体指定初始化成员使用点号 . 运算符和成员名。

比如我们可以让程序只初始化Book的price成员:

1
struct Book book = {.price = 48.8};

还可以不按结构体声明的成员顺序进行初始化:

1
2
3
4
5
struct Book book = {
.publisher = "清华大学出版社",
.price = 48.8,
.date = 20171111
};

内存对齐

struct 中会有内存对齐,存在比如每个成员的起始地址必须是其大小的倍数,整个 struct 大小是最大成员的倍数等规则。这其中的原因是什么呢?

C-基础语法_内存数据对齐

结构体数组

第一种方法是在声明结构体的时候进行定义:

1
2
3
4
struct 结构体名称
{
结构体成员;
} 数组名[长度];

第二种方法是先声明一个结构体类型(比如上面 Book),再用此类型定义一个结构体数组:

1
2
3
4
5
struct 结构体名称
{
结构体成员;
};
struct 结构体名称数组名[长度];

初始化结构体数组

1
2
3
4
5
struct Book book[3] = {
{"《第1本书》", "小甲鱼", 49.5, {2016, 11, 11}, "清华大学出版社"},
{"《第2本书》", "中甲鱼", 49.9, {2017, 10, 23}, "清华大学出版社"},
{"《第3本书》", "大甲鱼", 49.9, {2017, 11, 11}, "清华大学出版社"}
};

结构体指针

1
2
3
struct Book *pt // 声明
pt = &book;
// 结构体和数组不一样,名字不是指向第一个元素。所以赋值要使用取址运算符 &。

通过结构体指针访问结构体成员有两种方法:

  • (*结构体指针).成员名:用于结构体对象;
  • 结构体指针->成员名:用于指针。

八、二进制转换公式

转换公式
转换公式
转换公式
转换公式
转换公式
转换公式