参考: 里科《C和指针》
声明
1)int
short/long/signed/unsigned可以修饰类型。如果有以上的修饰,可以省略int;signed一般只用于char,因为其他整型在默认情况下是有符号数,但char是因编译器而异的。
2)float
只有long double,short/signed/unsigned不可用
3)数组
如果index是用已知的、正确值计算而来的,则无需检查是否合法;如果是根据用户输入的数据产生的,那么使用之前必须检查,以确保在有效范围中。编译器不会检查,要人工编码检查
4)指针
格式:类型 *名称;
为什么使用 int *a; 而非int* a?
因为 int* b, c, d; 并不是声明了3个int指针,而是b是指针,c和d是整型。因此,把类型单独写在前面,*跟随变量名是一种更清楚的写法。这里要声明3个指针应该写作:int *b, *c, *d;
声明一个指针,并用字符串常量初始化:
char *message = "Hello";
typedef:给某个数据类型一个新名字(方便记忆;如果要修改数据类型,直接修改typedef即可)
typedef int *int_pointer;
int_pointer ip; // int_pointer是一个指向int的指针
// 等价于
int *ip;
最好不要用#define创建新的类型名,因为可能无法正确地处理指针。
#define d_ptr_to_char char *
d_ptr_to_char a,b; // 可以正确声明a,但b不对。使用typedef可以直接用
常量
常量的值是不能修改的,加const即可。
int const a;
const int a; // 两种格式都ok,习惯这个
如何赋值?1)声明时直接初始化;2)函数中声明为const的形参在函数被调用时得到实参的值。
指针:指针的值和所指向的值是两个东西。记忆方法是const只管它右边邻居的值(当然const int和int const一致,看上面)
int *pi; // 指针变量
const int *pci; // 指向整型常量的指针。可以修改指针的值(指向别处),但不能修改它指向的int值
int const *pci; // 同上
int * const cpi; // 指向整型的常量指针。不能修改指针的值,但可以修改它指的int的值
const int* const cpci; // 不能改指向也不能改值
// 测试
int a = 1;
int b = 2;
const int *pi = &a;
pi = &b; // 可以修改指向
printf("%d\n", *pi); // 2
//*pi = 3; // 不是可修改的左值,报错。不能修改值
int* const cpi = &a;
*cpi = 3; // 可以修改值,不能修改指向
//cpi = &b; // 不是可修改的左值,报错
printf("%d\n", *cpi); // 3
const int* const cpci = &a; // 必须初始化
#define创建名字常量
#define MAX_ELEMENTS 50;
int const max_elements = 50;
此时使用#define更好,因为只要需要用到字面值常量的地方都可以使用前者,无论是否初始化一个变量。const变量只能用于允许使用变量的地方(虽然数组声明时可以用const作长度)。
使用名字常量的意义在于,它给一个数值起了符号名。尤其在定义数组长度、限制计数次数等情况下,能够提高程序的可维护性:如果一个值需要修改,只修改声明即可,比搜索整个程序修改字面值常量容易得多。
作用域
编译器可以确认4种作用域:文件作用域file scope、函数作用域function scope、代码块作用域block scope和原型作用域prototype scope。作用域的作用:1)域外无法访问 2)不同作用域可以有同名变量、函数。
注意:
- 应该避免在嵌套的代码块中出现跟外围变量同名的变量(因为程序只能用内部的值,外部的值被隐藏)。
如果在函数内声明了跟形参同名的变量,那么形参的值会被隐藏。直接报错了
任何在所有代码块之外声明的标识符都是文件作用域,表示从声明处到文件结尾都可以访问。所以如果在头文件中写了一些常量,这个头文件被include到其他文件后,这些常量也是可以访问的。
原型作用域只适用于函数原型(不是函数的定义)中声明的参数名。本来这些参数名不是必须的,也不必跟函数定义中的形参名匹配,
函数作用域只适用于语句标签。语句标签是goto用的,这个太少见了。要求是:一个函数中所有的语句标签必须唯一(方便跳转)
链接属性
如果相同的标识符出现在几个不同的源文件中,需要使用链接属性决定如何处理。
1)外部external:不论声明多少次、在几个源文件,都指向同一个实体。在其他语言里也称为全局实体(global)。而且external实体都是静态存储类型。
2)内部internal:在同一个源文件内的所有声明都指向一个实体,但不同源文件的属于不同的
3)无none:没写链接属性,多个声明都是独立的
代码块作用域的变量和函数定义(函数名)缺省是external的,如果希望改成internal,就要在声明前加static关键字。而且static只能在缺省是external时才能改变链接属性,比如e即使声明static也不是external的。
typedef char *a;
int b; // 默认external
int c( int d) // c默认external
{
int e;
int f( int g ); // f默认external
}
如果希望把某个某个标识符指定为external,那应该在第一次声明时使用extern关键字。如果在第2次或以后声明时采用,不会改变第一次声明时指定的链接属性。
static int i;
int func() {
extern int i; // 实际还是internal的
}
文件作用域的变量其实不用指定static还是extern,但是可以用,这样看起来更清晰。
存储类型storage class
指存储变量值的内存类型,决定了变量何时创建、何时销毁及值保持多久。存储变量的位置有:1)普通内存 2)运行时的堆栈 3)硬件寄存器
变量的缺省存储类型取决于声明位置:
1)在任何代码块外声明的变量存储在静态内存中,不属于堆栈,称为静态变量static。这些变量不能指定存储类型,是在程序运行之前创建的,在整个执行期间始终存在且保持值不变,除非给它赋值或程序结束。函数的存储类型无所谓,反正总是存储在静态内存中
2)在代码块内部声明的变量缺省存储类型是自动automatic,是存储在堆栈中的,称为自动变量auto。它在声明时才创建,代码块结束后销毁。如果加上关键字static,可以使之变成静态(指会一直存在,但不能改变链接属性,即作用域)。函数的形参不能声明为static(因为要传值啊)
3)register可以声明寄存器变量,表示存储机器的硬件寄存器而不是内存中。通常寄存器变量比在内存的访问效率更高,但是编译器不一定会执行(有自己的优化方法),因为如果有太多变量被声明为register,它可能只挑几个存储到寄存器,其余仍然跟auto一样存在堆栈。虽然寄存器变量的创建与销毁时间跟auto相同,但是常需要做额外工作,比如恢复先前存储的值(又用到堆栈),因此将指针或者函数的形参声明为register未必能节省很多时间
静态变量是在可执行文件载入内存时,变量就有正确的值了,如果没有显式初始化,则会初始化为0(初始化只有一次);但是自动变量的初始化开销大,除非显式初始化,不然创建后它的值都是垃圾。
作用域、存储类型实例
int a = 5; // a是external的,其他文件
extern int b; // 如果b定义在其他地方,类似a,此时extern不必须,但是加上可以体现
static int c; // c本来external,现在别的文件不能使用了
// a,b,c的存储类型都是静态,即存储在静态内存中
int d(int e) // d在缺省的情况下是external,所以其他源文件只要声明,就可以调用d
{
int f = 15;
register int b;
static int g = 20; // 定义为static后初始化一次,后续不再初始化
extern int a; // extern没必要,因为仍然在第一行的作用域内
...
{
int e; // 形参e被屏蔽,是auto
int a; // auto
extern int h; // 为了访问全局变量h,必须使用extern关键字,否则是局部变量
...
}
...
{
int x, e;
}
...
}
static int i(){} // 是internal,其他源文件不可调用。d不能调用i,因为d前没有i的声明