第十章 结构和联合
在C中,使用结构可以把不同类型的值存储在一起。
10.1 结构的基础知识
C 语言提供了两种聚合数据类型,数组和结构。
数组元素可以通过下标访问,这是因为数组的元素长度相同。但是结构中情况并非如此。由于一个结构的成员可能长度不同,所以不能使用下标来访问他们。
相反,每个成员都有自己的名字,它们是通过名字访问的。
这个区别非常重要。结构并非一个自身成员的数组。和数组名不同,当一个结构变量在表达式中使用时,它并不被替换成一个指针,
结构变量也无法使用下标来选择特定的成员。
结构变量属于标量类型。
结构声明
struct tag { member-list } variable-list;
例子
struct {
int a;
char b;
float c;
} x;
struct {
int a;
char b;
float c;
} y[20], *z;
警告:
这两个声明被编译器当做两种截然不同的类型,即使他们的成员列表完全相同,因此,变量y 和z 的类型和x的类型不同,所以下面这条语句
z = &x;
是非法的。
标签(tag)字段允许为成员列表提供一个名字,这样它就可以在后续的声明中使用。
struct SIMPLE {
int a;
char b;
float c;
};
标签标示了一种模式,用来声明未来的变量。
struct SIMPLE x;
struct SIMPLE y[20], *z;
现在 x, y, z 都是同一种类型的结构变量。
声明结构时可以使用另外一种良好的技巧,用typedef创建一个新的类型,如下
typedef struct{
int a;
char b;
float c;
} Simple;
这个技巧和声明一个结构标签的效果几乎相同,区别在于Simple现在是个类型名而不是一个结构标签,所以
Simple x;
Simple y[20], *z;
结构成员的直接访问
点操作符.
结构成员的间接访问
void func(struct COMPLEX *cp);
函数可以使用下面这个表达式来访问这个变量所指向的结构的成员f:
(*cp).f
这个概念有点惹人厌,所以C语言提供了一个更为方便的操作符来完成这项工作: ->操作符(也称箭头操作符)
cp -> f
结构自引用
struct SELF_REF1{
int a;
struct SELF_REF1 b; 不合法!!!
int c;
};
sruct SELFZ_REF2{
int a;
struct SELF_REF2 *b; 合法!!!!
int c;
}
警告:
警惕下面这个陷阱
typedef struct{
int a;
SELF_REF3 *b; 非法!!!
int c;
} SELF_REF3;
解决方案
typedef struct SELF_REF3_TAG{
int al
struct SELF_REF3_TAG *b;
int c;
} SELF_REF3;
不完整的声明
偶尔,你必须声明一些相互之间存在依赖的结构。
这个问题的解决方案是使用不完整声明。
下面这个例子,两个不同类型的结构内部都有一个指向另一个结构的指针。
struct B;
struct A{
struct B *partner;
/* other declarations */
};
struct B{
struct A *partner;
/* other declarations */
};
在A的成员列表中需要标签B的不完整声明。一旦A被声明以后,B的成员列表也可以被声明。
10.3 结构的储存分配
编译器按照成员列表的顺序一个接一个地给每个成员分配内存。只有当存储成员时需要满足正常的边界对齐要求时,成员之间才可能出现用于填充的额外内存空间。
系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对齐要求,因此所有结构的起始存储位置必须是结构中边界要求最严格的数据类型所要求的位置。
你可以在声明中对结构的成员列表重新排列,让那些对边界对齐要求严格的成员首先出现,对便捷要求最弱的成员最后出现。
这种做法可以最大限度地减少因边界对齐而带来的空间损失。
sizeof操作符能够得出一个结构的整体长度,包括因边界对齐而跳过的那些字节。
如果你必须确定结构某个成员的实际位置,因该考虑边界对齐因素,可以使用offsetof宏(定义于stddef.h)
size_t offsetof(type, member)
例如
offsetof(struct ALIGN,b) 的返回值是4.
10.4 作为函数参数的结构
把结构作为参数传递给一个函数是合法的,但这种做法往往并不适宜。
因为C语言的参数传值调用方式要求把参数的一份拷贝传递给函数。
传递给函数的可以是一个指向结构的指针。在许多机器上,你可以把参数声明为寄存器变量从而进一步提高指针传递方案的效率。
向函数传递指针的缺陷在于函数现在可以对调用程序的结构变量进行修改。如果我们不希望如此,可以在函数中使用const关键字来防止这类修改。
10.5 位段
关于结构,我们必须提到它们实现位段的能力。位段的声明和结构类似。但它的成员是一个或多个位的字段。
这些不同长度的字段实际上储存于一个或多个整型变量中。
位段的声明和任何普通结构成员声明相同,但有两个例外。首先,位段成员必须声明为int, signed int,unsigned int类型,
其次,在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占用的位的数目。
使用位段的好理由,它能够把长度为奇数的数据包装在一起,节省存储空间。
另一个使用位段的理由是由于它可以很方便的访问一个整型值的部分内容。
10.6 联合
联合的声明和结构类似,但它的行为方式却和结构不同。联合的所有成员引用的是内存中的相同位置。当你想在不同的东西存储于同一个位置时,就可以使用联合。
struct VARIABLE{
enum {INT, FLOAT, STRING} type;
union{
int i;
float f;
char *s;
} values;
};
变体记录
从概念上讲,这就是我们刚刚讨论过的那种情况--内存中某个特定的区域将在不同时刻存储不同的值。
但是,在现在这个情况下,这些值比简单的整型或浮点型更加复杂 。他们每一个都是一个完整的结构。
struct INVREC{
char partno[10];
int quan;
enum{ PART, SUBASSY } type;
union{
struct PARTINFO part;
struct SUBASSYINFO subassy;
} info;
};
更好的方法是在联合中存储指向不同成员的指针而不是直接存储成员本身。所有的指针长度都是相同的,这样就解决了内存浪费问题。
联合的初始化
联合变量可以被初始化,但这个初始值必须是联合第一个成员的类型,而且必须位于一对花括号里面。例如
union{
int a;
float b;
char c[4];
}x = {5};
把x.a初始化为5.
第十一章 动态内存分配
当你声明数组时,你必须用一个编译时常量指定数组的长度。但是,数组的长度常常在运行时才知道,这是因为他需要的内存空间取决于输入数据。
11.2 malloc 和 free
C 函数库提供了两个函数,malloc和free,分别用于执行动态内存分配和释放。这些函数维护一个可用的内存池。当一个程序需要一些内存时,它就调用malloc
函数,malloc从内存池中提取一块合适的内存,并向该函数返回一个指向这块内存的指针。这块内存此时并没有以任何方式进行初始化。当一块以前分配的内存
不在使用时,程序调用free函数把它归还给内存池以供后备之需。
声明于 stdlib.h
void *malloc(size_t size);
void free(void *pointer);
malloc 的参数是需要分配的字节数。
malloc 所分配的是一块连续的内存。如果内存池是空的,或者它的可用内存无法满足你的请求,这种情况下,malloc 会向操作系统请求,要求得到更多的内存,
并在这块新内存上执行分配任务。如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针。
因此,对每一个从malloc 返回的指针都要进行检查,确保它并非NULL是非常重要的。
free 的参数必须是NULL,要么是一个先前从 malloc, calloc 或 realloc 返回的值。
11.3 calloc 和 realloc
void *calloc(size_t num_element, size_t element_size);
void realloc(void *ptr, size_t new_size);
calloc和malloc的主要区别是前者在返回指向内存的指针之前把它初始化为 0.
realloc 函数用于修改一个已经分配的内存块的大小。使用这个函数,你可以使一块内存扩大或缩小。如果它用于扩大一个内存块,那么这块内存原先的内存依然
保留,新增的内存添加到原先内存块的后面,新内存并未以任何方式初始化。如果它用于缩小一个内存块,该内存块尾部的部分内存就被拿掉,剩余的部分内存的原先
内容依然保留。
在使用realloc之后,你就不能再使用指向旧内存的指针,而是应该改用 realloc 所返回的新指针。
11.4 使用动态内存分配的内存
int *pi;
...
pi = malloc(100);
if(pi == NULL){
printf("Out of memory!\n");
exit(1);
}
提示
如果你的目标是获得足够存储25个整数的内存,更好的技巧是
pi = malloc(25 * sizeof(int));
这个方法好些,因为它是可移植的。
11.5 常见动态内存错误
包括对NULL指针进行解引用,对分配的内存进行操作时越界,释放非动态内存分配的内存,试图释放一块动态分配的内存的一部分
以及一块动态内存被释放后被继续使用。
内存泄露
当动态分配的内存不再需要使用时,它应该被释放,这样它以后可以被重新分配使用。分配内存,但在使用完毕后不释放将引起内存泄露。
在那些所有执行程序共享一个通用内存池的操作系统中,内存泄露将以一点点榨干可用的内存,最终使其一无所有。要摆脱这个困境,只有重启系统。
第十二章 使用结构和指针
使用结构和指针创造强大的数据结构---链表
12.1 链表
链表就是一些包含数据的独立数据结构(通常成为节点)的集合。链表中的每一个节点通过链或指针连接在一起。程序通过指针访问链表中的节点。
通常节点是动态分配的。
12.2 单链表
typedef struct NODE{
struct NODE *link;
int value;
}Node;
12.3 双链表
typedef struct NODE{
struct NODE *fwd;
struct NODE *bwk;
int value;
}Node;
第十三章 高级指针话题
13.1 进一步探讨指向指针的函数
int i;
int *pi;
int **ppi;
printf("%d\n", ppi); 如果ppi是个自动变量,他就未被初始化,这条语句打印一个随机值,如果它是一个静态变量,这条语句将打印 0。
printf("%d\n", &ppi); 这条语句把存储ppi的地址作为十进制整数打印出来,这个值并不是很有用。
*ppi = 5; 这条语句是不可预测的,对ppi不应该执行间接引用操作,因为它尚未被初始化。
13.2 高级声明
int* f, g; f是一个指针,而g是一个整型变量。
int f();
int *f(); f是一个函数,返回类型是一个指向整型的指针。
int (*f)(); f是一个函数指针,指向一个返回类型是整型的函数。
int *(*f)(); f也是一个函数指针,所指函数返回值是一个整型指针。
int f[];
int *f[]; f是个数组,它的元素类型是指向整型的指针。
int f()[]; 非法 !!!函数只能返回标量值,不能返回数组。
int f[](); 非法 !!!数组元素必须具有相同的长度,但不同的函数显然不可能具有不同的函数。
int (*f[])(); f是个数组,它的元素类型是函数指针,它所指向的函数的返回值是一个整型。
int *(*f[])(); 类似
完整的函数原型,例如:
int (*f)(int, float);
int *(*g[])(int, float);
int (*(*f)())[10]; f是个函数指针,指向的函数返回指向一个包含十个整数的数组的指针。
int (*(*f)[10])(); f是个数组,包含十个函数指针,指向返回值是整型的函数。
13.3 函数指针
两个用武之地:转换表/跳转表(jump table),作为参数传递给另一个函数(回调函数)。
声明和初始化
int f(int);
int (*pf)(int) = &f;
初始化表达式中的&操作符是可选的,因为函数名被使用时总是由编译器把它转化为函数指针。
&操作符只是显式的说明了编译器隐式执行的任务。
在函数指针被声明和初始化后,我们就可以使用三种方式调用函数:
int ans;
ans = f(25);
ans = (*pf)(25); 这三条语句是等效的
and = pf(25);
回调函数
待续...
转移表/跳转表 就是一个函数指针数组
例如
double add(double, double);
double sub(double, double);
double mul(double, double);
double div(double, double);
...
double (*oper_func[])(double, double) = {
add, sub, mul, div, ...
};
13.4 命令行参数
处理命令行参数是指向指针的指针的另一个用武之地。有些操作系统,包括UNIX和MS-DOS,让用户在命令行中编写参数来启动一个程序的执行。
int
main(int argc, int **argv);
例如
$ cc -c -o main.c insert.c -o test
argc == 7
argv[0] == "cc"
argv[1] == "-c"
argv[2] == "-o"
argv[3] == "main.c"
argv[4] == "insert.c"
argv[5] == "-o"
argv[6] == "test"
13.5 字符串常量
当一个字符串出现于表达式中时,它的值是个指针常量。编译器把这些指定字符的一份拷贝储存在内存的某个位置,并储存一个指向第一个字符的指针。
"xyz" + 1 结果是一个指针,指向第二个字符 y
*"xyz" 结果是第一个字符 'x'
"xyz"[2] 结果是字符 'z'
*("xyz" + 4) 越界,错误 !!!
在C中,使用结构可以把不同类型的值存储在一起。
10.1 结构的基础知识
C 语言提供了两种聚合数据类型,数组和结构。
数组元素可以通过下标访问,这是因为数组的元素长度相同。但是结构中情况并非如此。由于一个结构的成员可能长度不同,所以不能使用下标来访问他们。
相反,每个成员都有自己的名字,它们是通过名字访问的。
这个区别非常重要。结构并非一个自身成员的数组。和数组名不同,当一个结构变量在表达式中使用时,它并不被替换成一个指针,
结构变量也无法使用下标来选择特定的成员。
结构变量属于标量类型。
结构声明
struct tag { member-list } variable-list;
例子
struct {
int a;
char b;
float c;
} x;
struct {
int a;
char b;
float c;
} y[20], *z;
警告:
这两个声明被编译器当做两种截然不同的类型,即使他们的成员列表完全相同,因此,变量y 和z 的类型和x的类型不同,所以下面这条语句
z = &x;
是非法的。
标签(tag)字段允许为成员列表提供一个名字,这样它就可以在后续的声明中使用。
struct SIMPLE {
int a;
char b;
float c;
};
标签标示了一种模式,用来声明未来的变量。
struct SIMPLE x;
struct SIMPLE y[20], *z;
现在 x, y, z 都是同一种类型的结构变量。
声明结构时可以使用另外一种良好的技巧,用typedef创建一个新的类型,如下
typedef struct{
int a;
char b;
float c;
} Simple;
这个技巧和声明一个结构标签的效果几乎相同,区别在于Simple现在是个类型名而不是一个结构标签,所以
Simple x;
Simple y[20], *z;
结构成员的直接访问
点操作符.
结构成员的间接访问
void func(struct COMPLEX *cp);
函数可以使用下面这个表达式来访问这个变量所指向的结构的成员f:
(*cp).f
这个概念有点惹人厌,所以C语言提供了一个更为方便的操作符来完成这项工作: ->操作符(也称箭头操作符)
cp -> f
结构自引用
struct SELF_REF1{
int a;
struct SELF_REF1 b; 不合法!!!
int c;
};
sruct SELFZ_REF2{
int a;
struct SELF_REF2 *b; 合法!!!!
int c;
}
警告:
警惕下面这个陷阱
typedef struct{
int a;
SELF_REF3 *b; 非法!!!
int c;
} SELF_REF3;
解决方案
typedef struct SELF_REF3_TAG{
int al
struct SELF_REF3_TAG *b;
int c;
} SELF_REF3;
不完整的声明
偶尔,你必须声明一些相互之间存在依赖的结构。
这个问题的解决方案是使用不完整声明。
下面这个例子,两个不同类型的结构内部都有一个指向另一个结构的指针。
struct B;
struct A{
struct B *partner;
/* other declarations */
};
struct B{
struct A *partner;
/* other declarations */
};
在A的成员列表中需要标签B的不完整声明。一旦A被声明以后,B的成员列表也可以被声明。
10.3 结构的储存分配
编译器按照成员列表的顺序一个接一个地给每个成员分配内存。只有当存储成员时需要满足正常的边界对齐要求时,成员之间才可能出现用于填充的额外内存空间。
系统禁止编译器在一个结构的起始位置跳过几个字节来满足边界对齐要求,因此所有结构的起始存储位置必须是结构中边界要求最严格的数据类型所要求的位置。
你可以在声明中对结构的成员列表重新排列,让那些对边界对齐要求严格的成员首先出现,对便捷要求最弱的成员最后出现。
这种做法可以最大限度地减少因边界对齐而带来的空间损失。
sizeof操作符能够得出一个结构的整体长度,包括因边界对齐而跳过的那些字节。
如果你必须确定结构某个成员的实际位置,因该考虑边界对齐因素,可以使用offsetof宏(定义于stddef.h)
size_t offsetof(type, member)
例如
offsetof(struct ALIGN,b) 的返回值是4.
10.4 作为函数参数的结构
把结构作为参数传递给一个函数是合法的,但这种做法往往并不适宜。
因为C语言的参数传值调用方式要求把参数的一份拷贝传递给函数。
传递给函数的可以是一个指向结构的指针。在许多机器上,你可以把参数声明为寄存器变量从而进一步提高指针传递方案的效率。
向函数传递指针的缺陷在于函数现在可以对调用程序的结构变量进行修改。如果我们不希望如此,可以在函数中使用const关键字来防止这类修改。
10.5 位段
关于结构,我们必须提到它们实现位段的能力。位段的声明和结构类似。但它的成员是一个或多个位的字段。
这些不同长度的字段实际上储存于一个或多个整型变量中。
位段的声明和任何普通结构成员声明相同,但有两个例外。首先,位段成员必须声明为int, signed int,unsigned int类型,
其次,在成员名的后面是一个冒号和一个整数,这个整数指定该位段所占用的位的数目。
使用位段的好理由,它能够把长度为奇数的数据包装在一起,节省存储空间。
另一个使用位段的理由是由于它可以很方便的访问一个整型值的部分内容。
10.6 联合
联合的声明和结构类似,但它的行为方式却和结构不同。联合的所有成员引用的是内存中的相同位置。当你想在不同的东西存储于同一个位置时,就可以使用联合。
struct VARIABLE{
enum {INT, FLOAT, STRING} type;
union{
int i;
float f;
char *s;
} values;
};
变体记录
从概念上讲,这就是我们刚刚讨论过的那种情况--内存中某个特定的区域将在不同时刻存储不同的值。
但是,在现在这个情况下,这些值比简单的整型或浮点型更加复杂 。他们每一个都是一个完整的结构。
struct INVREC{
char partno[10];
int quan;
enum{ PART, SUBASSY } type;
union{
struct PARTINFO part;
struct SUBASSYINFO subassy;
} info;
};
更好的方法是在联合中存储指向不同成员的指针而不是直接存储成员本身。所有的指针长度都是相同的,这样就解决了内存浪费问题。
联合的初始化
联合变量可以被初始化,但这个初始值必须是联合第一个成员的类型,而且必须位于一对花括号里面。例如
union{
int a;
float b;
char c[4];
}x = {5};
把x.a初始化为5.
第十一章 动态内存分配
当你声明数组时,你必须用一个编译时常量指定数组的长度。但是,数组的长度常常在运行时才知道,这是因为他需要的内存空间取决于输入数据。
11.2 malloc 和 free
C 函数库提供了两个函数,malloc和free,分别用于执行动态内存分配和释放。这些函数维护一个可用的内存池。当一个程序需要一些内存时,它就调用malloc
函数,malloc从内存池中提取一块合适的内存,并向该函数返回一个指向这块内存的指针。这块内存此时并没有以任何方式进行初始化。当一块以前分配的内存
不在使用时,程序调用free函数把它归还给内存池以供后备之需。
声明于 stdlib.h
void *malloc(size_t size);
void free(void *pointer);
malloc 的参数是需要分配的字节数。
malloc 所分配的是一块连续的内存。如果内存池是空的,或者它的可用内存无法满足你的请求,这种情况下,malloc 会向操作系统请求,要求得到更多的内存,
并在这块新内存上执行分配任务。如果操作系统无法向malloc提供更多的内存,malloc就返回一个NULL指针。
因此,对每一个从malloc 返回的指针都要进行检查,确保它并非NULL是非常重要的。
free 的参数必须是NULL,要么是一个先前从 malloc, calloc 或 realloc 返回的值。
11.3 calloc 和 realloc
void *calloc(size_t num_element, size_t element_size);
void realloc(void *ptr, size_t new_size);
calloc和malloc的主要区别是前者在返回指向内存的指针之前把它初始化为 0.
realloc 函数用于修改一个已经分配的内存块的大小。使用这个函数,你可以使一块内存扩大或缩小。如果它用于扩大一个内存块,那么这块内存原先的内存依然
保留,新增的内存添加到原先内存块的后面,新内存并未以任何方式初始化。如果它用于缩小一个内存块,该内存块尾部的部分内存就被拿掉,剩余的部分内存的原先
内容依然保留。
在使用realloc之后,你就不能再使用指向旧内存的指针,而是应该改用 realloc 所返回的新指针。
11.4 使用动态内存分配的内存
int *pi;
...
pi = malloc(100);
if(pi == NULL){
printf("Out of memory!\n");
exit(1);
}
提示
如果你的目标是获得足够存储25个整数的内存,更好的技巧是
pi = malloc(25 * sizeof(int));
这个方法好些,因为它是可移植的。
11.5 常见动态内存错误
包括对NULL指针进行解引用,对分配的内存进行操作时越界,释放非动态内存分配的内存,试图释放一块动态分配的内存的一部分
以及一块动态内存被释放后被继续使用。
内存泄露
当动态分配的内存不再需要使用时,它应该被释放,这样它以后可以被重新分配使用。分配内存,但在使用完毕后不释放将引起内存泄露。
在那些所有执行程序共享一个通用内存池的操作系统中,内存泄露将以一点点榨干可用的内存,最终使其一无所有。要摆脱这个困境,只有重启系统。
第十二章 使用结构和指针
使用结构和指针创造强大的数据结构---链表
12.1 链表
链表就是一些包含数据的独立数据结构(通常成为节点)的集合。链表中的每一个节点通过链或指针连接在一起。程序通过指针访问链表中的节点。
通常节点是动态分配的。
12.2 单链表
typedef struct NODE{
struct NODE *link;
int value;
}Node;
12.3 双链表
typedef struct NODE{
struct NODE *fwd;
struct NODE *bwk;
int value;
}Node;
第十三章 高级指针话题
13.1 进一步探讨指向指针的函数
int i;
int *pi;
int **ppi;
printf("%d\n", ppi); 如果ppi是个自动变量,他就未被初始化,这条语句打印一个随机值,如果它是一个静态变量,这条语句将打印 0。
printf("%d\n", &ppi); 这条语句把存储ppi的地址作为十进制整数打印出来,这个值并不是很有用。
*ppi = 5; 这条语句是不可预测的,对ppi不应该执行间接引用操作,因为它尚未被初始化。
13.2 高级声明
int* f, g; f是一个指针,而g是一个整型变量。
int f();
int *f(); f是一个函数,返回类型是一个指向整型的指针。
int (*f)(); f是一个函数指针,指向一个返回类型是整型的函数。
int *(*f)(); f也是一个函数指针,所指函数返回值是一个整型指针。
int f[];
int *f[]; f是个数组,它的元素类型是指向整型的指针。
int f()[]; 非法 !!!函数只能返回标量值,不能返回数组。
int f[](); 非法 !!!数组元素必须具有相同的长度,但不同的函数显然不可能具有不同的函数。
int (*f[])(); f是个数组,它的元素类型是函数指针,它所指向的函数的返回值是一个整型。
int *(*f[])(); 类似
完整的函数原型,例如:
int (*f)(int, float);
int *(*g[])(int, float);
int (*(*f)())[10]; f是个函数指针,指向的函数返回指向一个包含十个整数的数组的指针。
int (*(*f)[10])(); f是个数组,包含十个函数指针,指向返回值是整型的函数。
13.3 函数指针
两个用武之地:转换表/跳转表(jump table),作为参数传递给另一个函数(回调函数)。
声明和初始化
int f(int);
int (*pf)(int) = &f;
初始化表达式中的&操作符是可选的,因为函数名被使用时总是由编译器把它转化为函数指针。
&操作符只是显式的说明了编译器隐式执行的任务。
在函数指针被声明和初始化后,我们就可以使用三种方式调用函数:
int ans;
ans = f(25);
ans = (*pf)(25); 这三条语句是等效的
and = pf(25);
回调函数
待续...
转移表/跳转表 就是一个函数指针数组
例如
double add(double, double);
double sub(double, double);
double mul(double, double);
double div(double, double);
...
double (*oper_func[])(double, double) = {
add, sub, mul, div, ...
};
13.4 命令行参数
处理命令行参数是指向指针的指针的另一个用武之地。有些操作系统,包括UNIX和MS-DOS,让用户在命令行中编写参数来启动一个程序的执行。
int
main(int argc, int **argv);
例如
$ cc -c -o main.c insert.c -o test
argc == 7
argv[0] == "cc"
argv[1] == "-c"
argv[2] == "-o"
argv[3] == "main.c"
argv[4] == "insert.c"
argv[5] == "-o"
argv[6] == "test"
13.5 字符串常量
当一个字符串出现于表达式中时,它的值是个指针常量。编译器把这些指定字符的一份拷贝储存在内存的某个位置,并储存一个指向第一个字符的指针。
"xyz" + 1 结果是一个指针,指向第二个字符 y
*"xyz" 结果是第一个字符 'x'
"xyz"[2] 结果是字符 'z'
*("xyz" + 4) 越界,错误 !!!