C语言的一些注意事项

文章目录
  1. 1. printf() 为什么需要输出控制符?
  2. 2. 一维数组名是变量吗?
  3. 3. 一维数组如何赋值?
  4. 4. 使用指针有什么好处?
  5. 5. 变量与 0 比较
  6. 6. 传统数组(静态数组)的缺陷
  7. 7. 动态内存分配
  8. 8. 内存释放
  9. 9. 内存的五个部分
  10. 10. static 修饰符
  11. 11. const 修饰指针的几个例子
  12. 12. extern 的用法
  13. 13. volatile 的用法
  14. 14. 预处理命令

1. printf() 为什么需要输出控制符?

输出控制符:%d、%c 等

答:计算机中任何信息都是以 0 1 0 1 组合形式存在的,对于同样的 0 1 组合信息,计算机不知道该组合数据是一个整数还是一个其他类型的数,所以必须有个控制符来告诉计算机。

2. 一维数组名是变量吗?

如:

1
2
3
int a[5]; 
int b[5] = {1, 3, 4, 5, 6};
a = b; // 错误!

答:一维数组名 a 是常量,它等于这个数组第一个元素的地址。
所以 a = b; 是错误的,因为常量不能被赋值。

其中a[0]、a[1] 元素等是变量。

3. 一维数组如何赋值?

  • 定义的同时进行赋值,只有这样才能整体赋值

int a[] = {1, 3, 12, 5}; //4个元素

int b[8] = {1, 3, 12, 5}; //8个元素,后5个为0

  • 先定义后赋值,必须逐个赋值了
1
2
3
4
int a[5];
for ( int i = 0; i < 5; i++ ) {
a[i] = i;
}

注意:这个性质与结构体相似,但是结构体可以通过强制类型转换实现后期整体赋值

一般情况:

1
2
3
4
5
6
7
typedef struct student {
char * name;
int grade;
int score;
} Student;

Student s1 = {"zhangsan", 5, 99};

先定义后赋值特殊处理办法:

1
2
3
Student s2;

s2 = (Student){"lisi", 4, 89};

4. 使用指针有什么好处?

指针即地址,地址即内存单元的编号,一字节为一单元。

对于 32 位机器,即 32 根地址总线,能够访问 2^32 = 4G 个地址单元,所以老电脑最大只能支持 4G 内存。

指针有什么好处:

  • 数据结构中,需要利用指针,比如链表、树、图;所以学习指针是为了数据结构打基础;
  • 指针可以快速地传递数据,并且节省内存。因为不用复制整个数据进行传输,而仅仅传输该数据的指针(地址)即可;
  • 被调函数可以利用指针返回多个值,如果没有指针只能返回一个值给主调函数;
  • 相较于字符数组,指针处理字符串更方便;
  • 指针可以直接访问硬件。

5. 变量与 0 比较

  • 整数 0
1
2
if ( p == 0 )
if ( p != 0 )
  • bool 类型
1
2
if ( p )
if ( !p )

bool 类型的 True 和 False 在 C 语言中是通过 #define True 1#define False 0 的形式定义的。如果写成 if ( p == True )就等价于 if ( p == 1 ),这就出问题了,因为实际上 bool 类型的 True 的实际含义是「非0的所有值,而非仅仅指1」。

  • 指针类型
1
2
if ( p == NULL )
if ( p != NULL )

虽然 NULL 值为 0,但是含义不同,NULL 表示的是内存单元的编号 0,即 0x0000000000000000 地址。

计算机规定了以 0 为编号的内存单元不可读、不可写。

6. 传统数组(静态数组)的缺陷

  • 内存空间的分配和释放是系统控制的。函数运行期间,系统为该函数内部的数组分配空间,待该函数运行完毕时,系统释放该数组内存空间。
  • 进而导致 A 函数一旦运行结束,那么其他 B 函数就无法使用 A 中的数组变量了,即静态数组无法跨函数使用。
  • c11 标准前,数组定义时必须指定长度,而且在函数运行过程中无法修改数组长度。

为了消除以上的缺陷,引入了动态数组,即动态内存分配的数组。

7. 动态内存分配

  • 什么叫动态内存分配

malloc() 函数原型:(void *)malloc( int len ),表示向系统申请 len 个字节的内存空间,如果申请成功则返回第一个字节的地址,如果失败,则返回 NULL。

  • 为什么要强制类型转换?

如:int * p = (int *)malloc(50);,向系统申请50个字节的内存空间,malloc 函数返回第一个字节的地址,但是这个地址是无意义的,需要转换为相应数据类型的地址才可以使用。

换言之就是,malloc 返回第一个字节的地址,经过(int *)强制类型转换后,返回的就是4个字节的地址,那么 p 指针变量,指向的就是这4个字节,而非一个字节。那么p + 1就指向了第二个4字节。

如:double * p = (double *)malloc(50),将 malloc 返回的第一个字节地址转换为 double * 型的地址,即将第一个字节的地址转换为8个字节地址,那么p + 1就指向了第二个8字节。

8. 内存释放

对于int * p = (int *)malloc(50);语句,系统分配了两块内存,一块是动态分配的50个字节的内存,一块是静态分配的 p 变量本身的内存(64位系统占用8字节)。

动态内存需要程序员手动释放:free(p)。静态内存只能由系统来释放,即 p 本身的内存只能在 p 变量所在的函数终止时由系统自动释放。

注意:函数运行中,free(p)释放了 p 指向的那个地址的内存,然后系统会将该地址交给其他程序使用。但是 p 变量本身的内存仍然存在,那么通过 p 依然可以找到被释放的那个内存,这就存在安全隐患了。

所以,不能对一个地址使用两次free(p),否则会破坏其他程序。

最好这么使用:

1
2
free(p);
p = NULL; // 拴住野指针,NULL 就是那条链子

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
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>

int main()
{
int localVar = 5; // 初始化一个局部变量
int * pLocal = &localVar; // 声明一个指针,并将其初始化为局部变量的地址

int * pHeap = new int; // 声明一个指针,将其指向一个int型堆内存
if ( nullptr == pHeap ) {
std::cout << "内存分配失败!" << std::endl;
return 1;
}

*pHeap = 7; // 将7存储到新分配的堆内存种

std::cout << "localVar: " << localVar << std::endl;
std::cout << "*pLocal: " << *pLocal << std::endl;
std::cout << "*pHeap: " << *pHeap << std::endl;

// 将 pHeap 指向其他堆内存
delete pHeap; // 1. 首先就要先释放掉pHeap原来指向的堆内存,不然就会造成内存泄漏。
pHeap = new int; // 2. 然后才能将其指向其他堆内存位置;
if ( nullptr == pHeap ) { // 用new分配内存后,紧接着就要用if检查是否分配成功
std::cout << "内存分配失败!" << std::endl;
return 1;
}

*pHeap = 9;

std::cout << "*pHeap: " << *pHeap << std::endl;

delete pHeap; // 不用了就释放掉堆内存
pHeap = nullptr; // 同时将pHeap指向空指针,否则就会变成野指针,存在安全问题

return 0;
}

9. 内存的五个部分

  1. 静态存储区:

    存储:全局变量、static 变量;

    生命周期:由编译器在编译时分配内存,整个程序结束后销毁;

    特点:编译时未赋值的变量系统会自动赋初值0(数值型变量)或空字符(字符变量);

  2. 栈:

    变量:局部变量;

    生命周期:函数结束后销毁;

    特点:效率高,空间有限;

  3. 堆:

    变量:由 malloc 系列函数(C语言)或 new 操作符(C++)分内存的变量;

    生命周期:程序员手动释放,由 free(C语言) 或 delete(C++) 决定;

    特点:使用灵活,空间比较大,但容易出错;

  4. 常量存储区:

    如:char * s = “Hello World”;

    特点:只读,无法修改;

  5. 程序代码区:

    程序运行时的函数体的二进制代码;

10. static 修饰符

  • 修饰全局变量 -> 静态全局变量:

    内存中的位置:静态存储区,不变;

    初始化:自动初始化为 0;

    作用域:只限于声明该变量的文件,而普通全局变量可以被所有源程序共享;

  • 修饰局部变量 -> 静态局部变量:

    内存中的位置:由「栈」变为了「静态存储区」;

    初始化:未经初始化情况下,由分配垃圾值 -> 自动初始化为 0;

    作用域:仍然为函数内部;

    生命周期:函数运行周期 -> 整个程序运行周期;

  • 修饰函数 -> 静态函数:

    这里的 static 的作用不是改变存储位置,而是同修饰全局变量一样,改变了函数的作用域,由所有源文件缩小为仅限于本文件。

    静态函数又称为内部函数,其优点:不同人编写不同函数时,不用担心其他人编写的文件里有同名函数。

总结:static 用来表示不能被其他文件访问的全局变量和函数,将在栈中分配的局部变量变为静态存储区的变量。

11. const 修饰指针的几个例子

const 修饰的变量或者函数,表示只读,不可更改。

const int * p; == int const * p;:*p 不可变(即 p 指向的对象不可变),但 p 可以变;

int * const p:p 不可变,p 指向的对象可变;

总结:const 离谁近,就修饰谁。

12. extern 的用法

使用场景:

在 a.c 文件中定义了函数 int fun(int nu)和全局变量 int v

要在 b.c 文件中使用 fun() 函数和变量 v;

那么其中一个方法就是使用 extern 来实现:在 b.c 文件中使用 extern int fun(int )extern int v进行外部声明,这样就可以在源文件 b.c 中使用源文件 a.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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
// a.c
#include <stdio.h>

int v;

int afun(int n)
{
printf("执行 a.c 文件中的 afun()\n");

return n;
}

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

extern int afun(int);

int bfun(int nu)
{
int n;

printf("执行 b.c 文件中的 bfun()\n");

n = afun(nu);

printf("从 a.c 回到 b.c: ");
printf("返回值:%d\n", n);

return n;
}

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

extern int bfun(int);

int main(void)
{
printf("执行 main()\n");

bfun(5);

return 0;
}

输出结果:

**去掉 b.c 中的extern int afun(int)**,输出结果为:

一般对其他模块中函数的引用,最常用的方法是包含这些函数声明的头文件

那么,两者有什么区别呢?请往下看:

  • 使用 extern 和包含头文件来引用函数有什么区别?

extern 相对来说的优点:

  1. 简洁。直截了当,想引用哪个函数就用 extern 声明哪个函数,符合 KISS 原则;

  2. 加速程序的编译过程,节省时间。在大型 C 程序编译过程中,这种差异相当明显。

此外, extern 修饰符可用于指示 C 或者 C++ 函数的调用规范。比如在 C++ 程序中用extern "C"声明要引用的函数。这是给链接器用的,告诉链接器在链接的时候使用 C 函数规范。主要原因是 C++ 和 C 程序编译完成后在目标代码中命名规则不同。

13. volatile 的用法

volatile 的中文意思是「易变的」,这里主要是对编译器说的,告诉编译器该变量易变,不要去优化包含该变量的代码,从而可以提供对特殊地址的稳定访问。

举例1:

1
2
3
int i = 10;
int j = i; // 语句1
int k = i; // 语句2

针对以上代码,编译器在编译时会进行优化,因为在 1、2 两条语句中,变量 i 没有被用作左值。

这时候编译器会认为 i 的值没有发生变化,所以执行语句 1 时从内存中取出 i 的值赋给变量 j 后,这个值并没有丢掉,而是在语句 2 时继续用这个值给变量 k 赋值。

编译器不会生成再次从内存中取 i 的值的汇编代码,这样就提高了效率。

举例2:

1
2
3
volatile int i = 10;
int j = i; // 语句3
int k = i; // 语句4

volatile 告诉编译器 i 值是可能随时变化的,每次使用它的时候必须从内存中重新读取 i 的值。

主要用在:i 是一个寄存器变量或者表示一个端口或者是多个线程的共享数据。因为这些地方 i 的值随时都可能变化。

14. 预处理命令

未完待续。。。