一、声明与定义
“声明”与“定义”最大的区别在于“定义”创建了一个对象并且为它分配了内存空间,而“声明”并没有为它分配内存空间。
二、内存空间
下图描述了内存空间中的一些相关概念。
栈:这是存储器用来保存局部变量的部分。每当调用函数,函数的所有局部变量都在栈上创建。该内存空间由编译器自动分配释放。其操作方式类似于数据结构中的栈。
堆:堆用于动态存储。程序在运行时创建的一些数据会保存在堆中。该内存空间由程序员分配释放,若程序员不释放,程序结束时可能由操作系统回收。其操作方式类似于数据结构中的链表,与数据结构中的堆要区分开。
全局量:又可以称作“静态区”。主要存储全局变量和局部静态变量。全局量位于函数之外,在程序开始运行时创建,对所有函数可见。程序结束后该内存空间由系统释放。
常量段:常量是在程序中要用到的不变量,也是在程序开始运行时创建。程序结束后由系统释放。
代码段:存储器中用来加载机器代码的部分。
在上述5个区域中,常量段与代码段都是只读的。
三、数组、指针和语法糖
声明char names[]
在不同的地方出现具有不同的意义。如果是普通的变量声明,names
就是一个数组,而且必须马上赋值,比如
int function(){
char names[] = "ABC";
...
}
但是如果names
以函数参数的形式声明,那么names
其实就是指针,它不过是*names
的语法糖。
void function(char names[]){
...
}
void function(char *names){
...
}
// 上面两种形式是等价的
四、数组与指针
先看下面两段代码:
#include <stdio.h>
int main()
{
char *cards = "JQK";
char a_card = cards[2];
cards[2] = cards[0];
cards[0] = cards[1];
cards[1] = a_card;
return 0;
}
#include <stdio.h>
int main()
{
char cards[] = "JQK";
char a_card = cards[2];
cards[2] = cards[0];
cards[0] = cards[1];
cards[1] = a_card;
return 0;
}
第二段代码可以正常地编译运行,但是根据运行环境的不同,第一段代码编译运行时会得到各种不同的错误。简单的看,问题的原因是“字符串无法更新”。
在第一段代码中,计算机存储器是这样运作的:
1. 计算机加载字符串字面值
当计算机把程序载入到存储器时,会把所有常量值(如字符串常量JQK
)放到常量存储区,这部分存储器是只读的。
2. 程序在栈上创建cards
变量
cards
是局部变量,被保存到栈中。
3. cards
变量指向”JQK“的地址
4. 计算机试图修改字符串
计算机试图修改cards
变量指向的字符串时会失败,因为字符串所在的常量区是只读的。
具体流程如下图示:
在第二段代码中,计算机存储器是这样运作的:
1. 计算机加载字符串字面值
当计算机把程序载入到存储器时,会把所有常量值(如字符串常量JQK
)放到常量存储区,这部分存储器是只读的。
2. 程序在栈上新建一个数组
由于声明了数组,程序会在栈上开辟一块内存空间给数组。
3. 程序初始化数组
程序将字符串字面值”JQK“内容复制到栈上。
4. 计算机修改字符串
程序对栈中的字符串副本进行修改,达到目的。
具体流程如下图示:
为了规避上述问题,在把指针设置为字符串字面值的时候,需要使用关键字const
:const char *s = "ABC"
,这样一来,如果编译器发现有代码试图修改字符串,就会提示编译错误。需要注意的是,加不加关键字const
,字符串字面值都是只读的,const
只是敲醒了编辑器。
五、再说几句
对于char s[] = "ABCD"
和char *t = "ABCD"
,
sizeof()
函数会有不同,sizeof(s) == 5
为数组在存储器中的长度而sizeof(t)
的大小等于该环境下char
的长度。
对于取址符号&
,&s
返回的是数组,即数组s的地址就是数组本身。&t
则表示char
型变量t
的地址。
当创建指针变量时,计算机会为它分配一定大小的内存空间。但如果创建的是一个数组,计算机会为数组分配内存空间,但不会为数组变量分配任何空间,编译器仅在它出现的地方把它替换为数组的起始地址。