第5章 指针、数组和结构

5.1 指针
指针,是一个无符号整数(unsigned int),它是一个以当前系统寻址范围为取值范围的整数。
 char c = 'a';
 char *cp = &c;//cp保存着c的地址

5.2 数组
数组就是相同数据类型的元素按一定顺序排列的集合。
int iarr [5];

5.2.1 字符串文字量
用双引号括起来的字符序列。例如 "hello"。
一个字符串文字量总是由一个空字符'\0'作为结束符。
sizeof("hello") == 6
字符串文字量的类型是“适当个数的const字符数组”。
字符串文字量是静态分配的,所以函数返回他们是安全的。


重要概念辨析:
(1)函数指针:实质是一个指针,该指针指向函数的入口地址。
void (*func)(int ,int);
(2)指针函数:本质是一个函数,函数返回类型是某一类型的指针。
void * func(int, int);
(3)数组指针:指向数组的指针,实质是一个指针,其指向的类型是数组。
int (*parr)[5];
(4)指针数组:元素为指针的数组,本质是一个数组,其中的元素为指针。
int * parr[5];



5.4 const
const表达“不变化的值”这样一个概念。
const是一个C语言的关键字,它限定一个变量不允许被改变。使用const在一定程度上可以提高程序的安全性和可靠性。
1什么是const?
常类型是指使用类型修饰符const说明的类型,常类型的变量或对象的值是不能被更新的。(当然,我们可以偷梁换柱进行更新)
2为什么引入const?

const 推出的初始目的,正是为了取代预编译指令,消除它的缺点,同时继承它的优点。
3主要作用
(1)可以定义const常量,具有不可变性。 
例如:const int Max=100; int Array[Max]; 
(2)便于进行类型检查,使编译器对处理内容有更多了解,消除了一些隐患。
例如: void f(const int i) { .........} 编译器就会知道i是一个常量,不允许修改; 
(3)可以避免意义模糊的数字出现,同样可以很方便地进行参数的调整和修改。 同宏定义一样,可以做到不变则已,一变都变!
如(1)中,如果想修改Max的内容,只需要:const int Max=you want;即可! 
(4)可以保护被修饰的东西,防止意外的修改,增强程序的健壮性。 还是上面的例子,如果在函数体内修改了i,编译器就会报错; 
例如: void f(const int i) { i=10;//error! } 
(5) 可以节省空间,避免不必要的内存分配。 例如: 
#define PI 3.14159 //常量宏 
const double Pi=3.14159; //此时并未将Pi放入RAM中 ...... 
double i=Pi; //此时为Pi分配内存,以后不再分配! 
double I=PI; //编译期间进行宏替换,分配内存 
double j=Pi; //没有内存分配 
double J=PI; //再进行宏替换,又一次分配内存! 
const定义常量从汇编的角度来看,只是给出了对应的内存地址,而不是象#define一样给出的是立即数,所以,const定义的常量在程序运行过程中只有一份拷贝,而#define定义的常量在内存中有若干个拷贝。 
(6) 提高了效率。 
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。


重要概念辨析:
(1)常量指针:只能读取内存中内容,不能够修改内存中内容的指针。
int const * ip;
(2)指针常量:指针所指向的地址不能改变,即指针本身是一个常量,但是指针所指向的内容可以改变。
int * const p=&a;
ps:指针常量必须在声明的同时对其初始化,不允许先声明一个指针常量随后再对其赋值,这和声明一般的常量是一样的。
简单理解--左定值,右定向
const在*号左边,表示指针指向的内容不能修改(常量指针)
const在*号右边,表示指针指向的地址不能修改(指针常量)


5.5 引用
一个引用就是某个对象的另一个名字。
引用的主要作用是为了描述函数的参数和返回值。
为了确保一个引用总能够使某个对象的名字,我们必须对引用初始化。
int i = 100;
int &r = i;


5.6 指向 void 的指针
一个指向任何对象类型的指针都可以赋值给 void*的变量,一个void*变量可以赋值给另一个void*变量,两个void*变量可以比较相等与否,一个void*变量可以显式的转换为另一个类型。
其他的操作都是不安全的(编译器不知道实际被指向的是哪种对象),因此,要使用void*变量,必须显式的将它转换为某个特定类型的指针。
void* 的最主要的用途是向函数传递一个指针。
int a = 100;
int * pi = &a;
void * pv = pi; //ok,从int*到void*的隐式转换
*pv; //err, void* 不能间接引用
pv++; //err, void* 不能自增(不知道被指向对象的大小)


5.7 结构
结构就是一个可以包含任意类型元素的集合。
struct Person
{
   char name[20];
   char sex[10];
   int age;


};//!!!冒号不能掉;


结构类型对象的大小是其成员的大小之和

重要概念辨析:

内存对齐
一、内存对齐的原因
大部分的参考资料都是如是说的:
1、平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;
某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
2、性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。
原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
也有的朋友说,内存对齐出于对读取的效率和数据的安全的考虑,我觉得也有一定的道理。
二、对齐规则
    每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。
比如32位windows平台下,VC默认是按照8bytes对齐的(VC->Project->settings->c/c++->Code Generation中的truct member alignment 值默认是8),
程序员可以通过预编译命令#pragma pack(n),n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
    在嵌入式环境下,对齐往往与数据类型有关,特别是C编译器对缺省的结构成员自然对届条件为“N字节对齐”,N即该成员数据类型的长度。
如int型成员的自然对界条件为4字节对齐,而double类型的结构成员的自然对界条件为8字节对齐。
若该成员的起始偏移不位于该成员的“默认自然对界条件”上,则在前一个节面后面添加适当个数的空字节。
C编译器缺省的结构整体的自然对界条件为:该结构所有成员中要求的最大自然对界条件。
若结构体各成员长度之和不为“结构整体自然对界条件的整数倍,则在最后一个成员后填充空字节。
    那么可以得到如下的小结:
类型 对齐方式(变量存放的起始地址相对于结构的起始地址的偏移量)
Char    偏移量必须为sizeof(char)即1的倍数
Short   偏移量必须为sizeof(short)即2的倍数
int     偏移量必须为sizeof(int)即4的倍数
float   偏移量必须为sizeof(float)即4的倍数
double  偏移量必须为sizeof(double)即8的倍数
   各成员变量在存放的时候根据在结构中出现的顺序依次申请空间,同时按照上面的对齐方式调整位置,空缺的字节编译器会自动填充。
同时为了确保结构的大小为结构的字节边界数(即该结构中占用最大空间的类型所占用的字节数)的倍数,
所以在为最后一个成员变量申请空间后,还会根据需要自动填充空缺的字节,也就是说:结构体的总大小为结构体最宽基本类型成员大小的整数倍,
如有需要编译器会在最末一个成员之后加上填充字节。对于char数组,字节宽度仍然认为为1。
   对于下述的一个结构体,其对齐方式为:
struct Node1{
    double m1;
    char m2;
    int m3;
};
  对于第一个变量m1,sizeof(double)=8个字节;接下来为第二个成员m2分配空间,
这时下一个可以分配的地址对于结构的起始地址的偏移量为8,是sizeof(char)的倍数,
所以把m2存放在偏移量为8的地方满足对齐方式,该成员变量占用 sizeof(char)=1个字节;
接下来为第三个成员m3分配空间,这时下一个可以分配的地址对于结构的起始地址的偏移量为9,
不是sizeof (int)=4的倍数,为了满足对齐方式对偏移量的约束问题,自动填充3个字节(这三个字节没有放什么东西),
这时下一个可以分配的地址对于结构的起始地址的偏移量为12,刚好是sizeof(int), 
由于8+4+4 = 16恰好是结构体中最大空间类型double(8)的倍数,所以sizeof(Node1) =16.
 
typedef struct{
    char a;
    int b;
    char c;
}Node2;
    成员a占一个字节,所以a放在了第1位的位置;由于第二个变量b占4个字节,为保证起始位置是4(sizeof(b))的倍数,
所以需要在a后面填充3个字节,也就是b放在了从第5位到第8位的位置,然后就是c放在了9的位置,此时4+4+1=9。
接下来考虑字节边界数,
9并不是最大空间类型int(4)的倍数,应该取大于9且是4的的最小整数12,所以sizeof(Node2) = 12.
typedef struct{
    char a;
    char b;
    int c;
}Node3;
   明显地:sizeof(Node3) = 8
   对于结构体A中包含结构体B的情况,将结构体A中的结构体成员B中的最宽的数据类型作为该结构体成员B的数据宽度,
同时结构体成员B必须满足上述对齐的规定。
   要注意在VC中有一个对齐系数的概念,若设置了对齐系数,那么上述描述的对齐方式,则不适合。
   例如:
1字节对齐(#pragma pack(1))
输出结果:sizeof(struct test_t) = 8 [两个编译器输出一致]
分析过程:
成员数据对齐
#pragma pack(1)
struct test_t {
    int a; 
    char b; 
    short c;
    char d; 
};
#pragma pack()
成员总大小=8;
 
2字节对齐(#pragma pack(2))
输出结果:sizeof(struct test_t) = 10 [两个编译器输出一致]
分析过程:
成员数据对齐
#pragma pack(2)
struct test_t {
    int a; 
    char b; 
    short c;
    char d; 
};
#pragma pack()
成员总大小=9;
 
4字节对齐(#pragma pack(4))
输出结果:sizeof(struct test_t) = 12 [两个编译器输出一致]
分析过程:
1) 成员数据对齐
#pragma pack(4)
struct test_t { //按几对齐, 偏移量为后边第一个取模为零的。
int a; 
char b; 
short c;
char d; 
};
#pragma pack()
成员总大小=9;

 

posted on 2014-01-07 16:22  love so much  阅读(255)  评论(0编辑  收藏  举报

导航