C陷阱和缺陷--第三章 “语义陷阱”

本文详细阐述了C语言中指针与数组的使用,包括数组的静态定义、指针运算、内存管理、字符串操作、边界计算原则、函数参数处理、运算符顺序、整数溢出和main函数返回值的重要性。
摘要由CSDN通过智能技术生成
一个句子哪怕其中的每个单词都拼写正确,而且语法也无懈可击,任然可能有歧义或者并非书写着希望表达的意思。程序也有可能表面看上去是一个意思,而实际是哪个的意思却想去甚远。

3.1 指针与数组

1:数组的大小必须在编译期就作为一个常数确定下来,不能动态分配数组;
2:对于数组,我们只能够做两件事:确定该数组的大小,以及获得指向该数组下标为0的元素的指针;
3:任何一个数组下标运算,都等同于一个对应的指针运算;
4:如果 array_test 不是用于sizeof的操作数,那么 array_test 总是被转换成一个指向 array_test 数组的起始元素(下标为0)的指针;

int array_test[12][31] = {0};
sizeof(array_test) 等于 12*31*sizeof(int),而不是 指针的大小

char test[] = "hello";
printf("%s\n", test);
// 等价于
printf("%s\n", &test[0]);
int a[3];
int *p;
// 把数组a中下标为0的元素的地址赋值给P
p=a;
// 报错, &a 是一个指向数组的指针,p是一个指向整形变量的指针
p=&a

// 建议使用 下标来操作数组中的元素,不要使用指针操作
*a = 1; 等价于 a[0] = 1;

二维数组

int calendar[12][31];
int (*month_p)[31];
int *p;

month_p = calendar;	// 正确写法
p = calendar;				// 报错,指针类型不一致

3.2 非数组的指针

字符串常量代表代表了一块包括字符串中所有字符以及一个空字符(‘\0’)的内存区域的地址;

将字符串 s 和 字符串 t,连接成单个字符串 r

// 定义一个字符指针,在不确认 s+t 的大小情况下,不能直接分配空间
char *r;			
//  字符串有结束符,一定要多申请一个
r = malloc(strlen(s) + strlen(t) +1);
if (r == NULL)
	return;			// 没有申请到内存空间
strcpy(r, s);		// 把 s 指向的字符串, 复制到 r
strcat(r, t);		// 把 t 指向的字符串, 复制到 r 的结尾
// 结束一定要释放内存空间
free();

3.3 作为参数的数组声明

C语音会自动的将作为参数的数组声明,转换为相应的指针声明;因此,将数组作为函数参数毫无意义

int strlen(char s[])
{
}
// 等价于
int strlen(char *s)
{
}

如果一个指针参数并不实际代表一个数组,采用数组形式的记法经常会起到误导作用;

extern char *hello;		// 表示一个指针
extern char hello[];		// 表示一个数组

main 函数的定义
argv 是一个指向某数组的起始元素的指针,该数组的元素为字符指针类型;

int main(int argc, char* argv[]);
// 等价于
int main(int argc, char** argv);

3.4 避免 举隅法

举隅法:以含义更宽泛的词语来代替含义相对较窄的词语,或者相反; 对应C语言中一个常见的陷阱-混淆指针与指针所指向的数据;
复制指针并不同时复制指针所指向的数据

char *p,*q;
p="xyz";	//
// p和q 同时指向 一段 xyz\0 的内存空间
q=p;

3.5 空指针并非空字符串

常数0 常用一个符号来代替
#define NULL 0

当我们将 0 赋值给一个指针变量时,绝对不能企图使用该指针所指向的内存中存储的内容;

if (p == (char *)0)	// 合法的
if (strcmp(p, (char *)0) == 0)		// 非法的,因为 strcmp 的实现会去查看指针参数所指向的内容

3.6 边界计算与不对称边界

边界计算原则
原则一:首先考虑最简单情况下的特例,然后将得到的结果外推;
原则二:仔细计算边界,绝不掉以轻心;
边界计算方法:用第一个入界点和第一个出界点来表示一个数值范围;

// 不推荐写法
if ((x>=1) && (x<=8))		// 1-2-3-4-5-6-7-8, 共8个数
// 推荐写法
if ((x>=1) && (x<9))			// 9-1=8

针对数组计算边界
边界计算方法,对数组类型的数据计算特别便利

int a[10], i;
for (i=0; i<10;i++)	// 正确写法

for (i=0; i<=9;i++)	// 错误写法

针对缓冲区计算边界
根据边界计算方法,很容易的计算缓冲区中已使用内存,未使用内存等参数;

#define MAX_SIZE		1024
char buffer[MAX_SIZE];
char *ptr = &buffer[0];
*ptr++ = c;

// 缓冲区已存放的字节数 
saved = ptr - buffer;
// 缓冲区存满
if ((ptr - buffer) == max_size)
// 缓冲区未使用字符数
no_use = max_size - (ptr - buffer);

3.7 求值顺序

C语言中只有四个运算符(&&, ||, ?: 和 ,)存在规定的求值顺序。其它所有运算符对齐操作数的求值顺序,都是未定义的;
运算符 &&和|| 首先对左侧操作数求值,只在需要时才对右侧操作数求值。
运算符 ?: 有三个 操作数,在a?b:c 中,操作数 a首先被求值,根据a的值再去操作 b或者c;
逗号运算符,首先对左侧操作数求值,然后该值被丢弃,再对右侧操作数求值;

//两个参数, x和y 没有先后顺序
fun1(x, y);

//一个参数,先x后y
func2((x,y))
// 如果此时不先对 y!=0求值,那么当y=0是,x/y 则是非法的
if (y!=0  && x/y)
	func3();
int i=0;
while (i<n)
	y[i] = x[i++];		//异常代码,假设了 y[i]的地址,将在i的自增操作执行之前被求值

// 正确写法
while(i<n)
{
	y[i] = x[i];		 // 不要写容易被误解的代码,编译器也可能会误解
	i++;
}

3.8 运算符 &&,||和!

不要混用& 和 &&,&&是有先后执行顺序的

// 当i的值< tabsize时候,才会去计算 tab[i]
while (i < tabsize && tab[i] != x)
// 无论i值如何,都会去操作 tab[i]; 失去了原先的含义
while (i < tabsize & tab[i] != x)

3.9 整数溢出

当操作两个有符号数的时候,就有可能发生 溢出,产生预期之外的结果;
当发生溢出时,所有关于结果如何的假设都不再可靠;因为不用的编译器上对溢出处理的方式不一致

void func5()
{
    char a=10;
    char b=20;
    char c = 10*20;

    printf("c= %d \n",c );		// 溢出了,c= -56
}

3.10 为函数main提供返回值

要习惯养成给main函数设定返回值;当A程序被B程序调用的时候,在main里面设定返回值,B程序才能知道A程序是执行成功还是失败的;

// A程序
main()
{
	// something, 当程序发生崩溃是,B程序也无法得知
}

int main()
{
	// something
	return 0;			// 只有当 main函数返回0时,B程序才判断A程序返回成功
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值