这似乎是一个很凝重的话题,但是它真的很有趣。
1. 指针是指向某一类型的东西,任何一个整体,只要能称为整体就能拥有它自己的独一无二的指针类型,所以指针的类型其实是近似无穷无尽的
2. 函数名在表达式中总是以函数指针的身份呈现,除了取地址运算符以及sizeof
3. C语言最晦涩难明的就是它复杂的声明: void (*signal(int sig, void (*func)(int)))(int)
,试试着把它改写成容易理解的形式
4. 对于指针,尽最大的限度使用const
保护它,无论是传递给函数,还是自己使用
先来看看一个特殊的指针,姑且称它为指针,因为它依赖于环境: NULL
,是一个神奇的东西。先附上定义,在编译器中会有两种NULL(每种环境都有唯一确定的NULL):
#define NULL 0
#define NULL ((void*)0)
有什么区别吗?看起来没什么区别都是0
,只不过一个是常量,一个是地址为0的指针。
当它们都作为指针的值时并不会报错或者警告,即编译器或者说C标准认为这是合法的:
int* temp_int_1 = 0; //无警告
int* temp_int_2 = (void*)0; //无警告
int* temp_int_3 = 10; //出现警告
为什么?为什么0
可以赋值给指针,但是10
却不行?他们都是常量。
因为C语言规定当处理上下文的编译器发现常量0
出现在指针赋值的语句中,它就作为指针使用,似乎很扯淡,可是却是如此。
回到最开始,对于NULL
的两种情况,会有什么区别?拿字符串来说,实际上我是将字符数组看作是C风格字符串。
在C语言中,字符数组是用来存储一连串有意义的字符,默认在这些字符的结尾添加'\0'
,好这里又出现了一个0值。
对于某些人,在使用字符数组的时候总是分不清楚NULL
与'\0'
的区别而误用,在字符数组的末尾使用NULL
是绝对错误的!虽然它们的本质都是常量0,但由于位置不同所以含义也不同。
#####开胃菜已过 对于一个函数,我们进行参数传递,参数有两种形式: 形参与实参
int function(int value)
{
/*...*/
}
//...
function(11);
其中,value
是形参,11
是实参,我们知道场面上,C语言拥有两种传递方式:按值传递和按址传递,但是你是否有认真研究过?这里给出一个实质,其实C语言只有按值传递,所谓按址传递只不过是按值传递的一种假象。至于原因稍微一想便能明白。
对于形参和实参而言两个关系紧密,可以这么理解总是实参将自己的一份拷贝传递给形参,这样形参便能安全的使用实参的值,但也带给我们一些麻烦,最经典的交换两数
void swap_v1(int* val_1, int* val_2)
{
int temp = *val_1;
*val_1 = *val_2;
*val_2 = *val_1;
}
这就是所谓的按址传递,实际上只是将外部指针(实参)的值做一个拷贝,传递给形参val_1
与val_2
,实际上我们使用:
#define SWAP_V2(a, b) (a += b, b = a - b, a -= b)
#define SWAP_V3(x, y) {x ^= y; y ^= x; x ^= y}
试一试是不是很神奇,而且省去了函数调用的时间,空间开销。上述两种写法的原理实质是一样的。
但是,动动脑筋想一想,这种写法真的没有瑕疵吗?如果输入的两个参数本就指向同一块内存,会发生什么?
...
int test_1 = 10, test_2 = 100;
SWAP_V2(test_1, test_2);
printf("Now the test_1 is %d, test_2 is %d\n", test_1, test_2);
.../*恢复原值*/
SWAP_V2(test_1, test_1);
printf("Now the test_1 is %d\n", test_1);
会输出什么?:
$: Now the test_1 is 100, test_2 is 10
$: Now the test_1 is 0
对,输出了0,为什么?稍微动动脑筋就能相通,那么对于后面的SWAP_V3
亦是如此,所以在斟酌之下,解决方案应该尽可能短小精悍:
static inline void swap_final(int* val_1, int* val_2)
{
if(val_1 == val_2)
return;
*val_1 ^= *val_2;
*val_2 ^= *val_1;
*val_1 ^= *val_2;
}
#define SWAP(x, y) \
do{ \
if(&x == &y) \
break; \
x ^= y; \
y ^= x; \
x ^= y; \
}while(0)
这便是目前能找到最好的交换函数,我们在此基础上可以考虑的更深远一些,如何让这个交换函数更加通用?即适用范围更大?暂不考虑浮点类型。 提示:可用void*
与上面的情况类似,偶尔的不经意就会造成严重的后果:
int combine_1(int* dest, int* add)
{
*dest += *add;
*dest += *add;
return *dest;
}
int combine_2(int* dest, int* add)
{
*dest += 2 * (*add);//在不确定优先级时用括号是一个明智的选择
return *dest;
}
上述两个函数的功能一样吗?恩看起来是一样的
int test_3 = 10, test_4 = 100;
combine_1(&test_3, &test_4);
printf("After combine_1, test_3 = %d\n",test_3);
.../*恢复原值*/
combine_2(&test_3, &test_4);
printf("After combine_2, test_3 = %d\n",test_3);
输出
$: After combine_1, test_3 = 210
$: After combine_2, test_3 = 210
如果传入两个同一对象呢?
... /*恢复test_3原值*/
combine_1(&test_3, &test_3);
printf("After second times combine_1, test_3 = %d\n",test_3);
...
combine_2(&test_3, &test_3);
printf("After second times combine_2, test_3 = %d\n",test_3);
输出
$: After second times combine_1, test_3 = 40
$: After second times combine_2, test_3 = 30
知道真相总是令人吃惊,指针也是那么令人又爱又恨。
restrict
,被用于修饰指针,它并没有太多的显式作用,甚至加与不加,在 你自己 看来,效果毫无区别。但是反观标准库的代码中,许多地方都使用了该关键字,这是为何
#####关于数组的那些事 数组和指针一样吗?
不一样
要时刻记住,数组与指针是不同的东西。但是为什么下面代码是正确的?
int arr[10] = {10, 9, 8, 7};
int* parr = arr;
我们还是那句话,结合上下文,编译器推出 arr
处于赋值操作符的右侧,默默的将他转换为对应类型的指针,而我们在使用arr
时也总是将其当成是指向该数组内存块首位的指针。
//int function2(const int test_arr[10]
//int function2(const int test_arr[]) 考虑这三种写法是否一样
int function2(const int* test_arr)
{
return sizeof(test_arr);
}
...
int size_out = sizeof(arr);
int size_in = function2(arr);
printf("size_out = %d, size_in = %d\n", size_out, size_in);
输出: size_out = 40, size_in = 8
这就是为什么数组与指针不同的原因所在,在外部即定义数组的代码块中,编译器通过上下文发觉此处arr是一个数组,而arr
代表的是一个指向10个int类型的数组的指针,只所谓最开始的代码是正确的,只是因为这种用法比较多,就成了标准的一部分。就像世上本没有路,走的多了就成了路。"正确"的该怎么写
int (*p)[10] = &arr;
此时p
的类型就是一个指向含有10个元素的数组的指针,此时(*p)[0]
产生的效果是arr[0]
,也就是parr[0]
,但是(*p)
呢?这里不记录,结果是会溢出,为什么?
这就是数组与指针的区别与联系,但是既然我们可以使用像parr
这样的指针,又为什么要写成int (*p)[10]
这样丑陋不堪的模式呢?原因如下:
回到最开始说过的传递方式,按值传递在传递arr
时只是纯粹的将其值进行传递,而丢失了上下文的它只是一个普通指针,只不过我们程序员知道它指向了一块有意义的内存的起始位置,我想要将数组的信息一起传递,除了额外增加一个参数用来记录数组的长度以外,也可以使用这个方法,传递一个指向数组的指针 这样我们就能只传递一个参数而保留所有信息。但这么做的也有限制:对于不同大小,或者不同存储类型的数组而言,它们的类型也有所不同
int arr_2[5];
int (*p_2)[5] = &arr_2;
float arr_3[5];
float (*p_3)[5] = &arr_3;
如上所示,指向数组的指针必须明确指定数组的大小,数组存储类型,这就让指向数组的指针有了比较大的限制。
这种用法在多维数组中使用的比较多,但总体来说平常用的并不多,就我而言,更倾向于使用一维数组来表示多维数组,实际上诚如前面所述,C语言是一个非常简洁的语言,它没有太多的废话,就本质而言C语言并没有多维数组,因为内存是一种线性存在,即便是多维数组也是实现成一维数组的形式。
就多维数组在这里解释一下。所谓多维数组就是将若干个降一维的数组组合在一起,降一维的数组又由若干个更降一维的数组组合在一起,直到最低的一维数组,举个例子:
int dou_arr[5][3]; 就这个二维数组而言,将5个每个为3个int
类型的数组组合在一起,要想指向这个数组该怎么做?
int (*p)[3] = &dou_arr[0];
int (*dou_p)[5][3] = &dou_arr;
int (*what_p)[3] = dou_arr;
实际上多维数组只是将多个降一维的数组组合在一起,令索引时比较直观而已。当真正理解了内存的使用,反而会觉得多维数组带给自己更多限制 对于第三句的解释,当数组名出现在赋值号右侧时,它将是一个指针,类型则是 指向该数组元素的类型,而对于一个多维数组来说,其元素类型则是其降一维数组,即指向该降一维数组的指针类型。这个解释有点绕,自己动手写一写就好很多。
对于某种形式下的操作,我们总是自然的将相似的行为结合在一起考虑。考虑如下代码:
int* arr_3[5] = {1, 2, 3, 4, 5};
int* p_4 = arr_3;
printf("%d == %d == %d ?\n", arr_3[2], *(p_4 + 2), *(arr_3 + 2));
输出: 3 == 3 == 3 ?
实际上对于数组与指针而言, []
操作在大多数情况下都能有相同的结果,对于指针而言*(p_4 + 2)
相当于p_4[2]
,也就是说[]
便是指针运算的语法糖,有意思的是2[p_4]
也相当于p_4[2]
,"Iamastring"[2] == 'm'
,但这只是娱乐而已,实际中请不要这么做,除非是代码混乱大赛或者某些特殊用途。 在此处,应该声明的是这几种写法的执行效率完全一致,并不存在一个指针运算便快于[]
运算,这些说法都是上个世纪的说法了,随着时代的发展,我们应该更加注重代码整洁之道
在此处还有一种奇异又实用的技巧,在char数组中使用指针运算进行操作,提取不同类型的数据,或者是在不同类型数组中,使用char*
指针抽取其中内容,才是显示指针运算的用途。但在使用不同类型指针操作内存块的时候需要注意,不要操作无意义的区域或者越界操作。
实际上,最简单的安全研究之一,便是利用溢出进行攻击。
**Advance:**对于一个函数中的某个数组的增长方向,总是向着返回地址的,中间可能隔着许多其他自动变量,我们只需要一直进行溢出试验,直到某一次,该函数无法正常返回了!那就证明我们找到了该函数的返回地址存储地区,这时候我们可以进行一些操作,例如将我们想要的返回地址覆盖掉原先的返回地址,这就是所谓的溢出攻击中的一种。