C 语言的一些注意事项

2022-05-01 77点热度 0人点赞 0条评论

同步发布于「破壳AI个人网站」 以及微信公众号:「破壳AI」。

C 语言的一些注意事项

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

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

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

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

如:

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

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

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

一般情况:

typedef struct student {
    char * name;
    int grade;
    int score;
} Student;

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

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

Student s2;

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

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

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

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

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

5. 变量与 0 比较

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

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

  • 指针类型
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),否则会破坏其他程序。

最好这么使用:

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

9. 内存的五个部分

  1. 静态存储区:

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

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

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

  2. 栈:

    变量:局部变量;

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

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

  3. 堆:

    变量:由 malloc 系列函数或 new 操作符分内存的变量;

    生命周期:程序员手动释放,由 free 或 delete 决定;

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

  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 的用法

未完待续。。。

订阅博客,及时获取文章更新邮件通知

close

订阅博客,及时获取文章更新邮件通知

古月弧

保持专注,持续进步。

文章评论