指针(四)
因为前期在学驱动,所以花了一天时间借鉴了别的资料,把本科学的C语言捡起来。
指针的基本概念
堆栈有栈顶指针,队列有头指针和尾指针,这些概念中的"指针"本质上是一个整数,是数组的索引,通过指针访问数组中的某个元素,经过学习我们在间接寻址那里看到了另一个指针的概念,把一个变量所在的内存单元的地址保存在另外一个内存单元中,保存地址的这个内存单元称为指针,通过指针和间接寻址访问变量,这种指针在C语言中可以用一个指针类型的变量表示,例如某程序中定义了以下全局变量:
int *pi=&i;表示定义一个指向int型的指针变量pi,并用i的地址来初始化pi.
int i;
int *pi = &i;
char c;
char *pc = &c;
注意:
后面两行代码定义了一个字符型变量c和一个指向c的字符型指针pc,注意pi和pc虽然是不同类型的指针变量,但它们的内存单元都占4个字节,因为要保存32位的虚拟地址,同理,在64位平台上指针变量都占8个字节。
pi是int型的,pc是char型的,pi=pc;这样赋值就是错误的。但是可以先强制类型转换然后赋值:
pi = (int *)pc;
但是:
**
现在pi指向的地址和pc一样,但是通过pc只能访问到一个字节,而通过pi可以访问到4个字节,后3个字节已经不属于变量c了,除非你很确定变量c的一个字节和后面3个字节组合而成的int值是有意义的,否则就不应该给pi这么赋值。因此使用指针要特别小心,很容易将指针指向错误的地址。
**
PI的地址和PC的地址一样了,这样就导致了可以访问的只有一个字节的地址了。原来是4字节。
为什么会出现野指针?
因为堆栈上分配变量的地址是随机的,也就是指针变量P所指向的内存地址也是不确定的。所以这种指向不确定地址的指针成为 “ 野指针 ”,为了避免野指针,在定义指针变量时就应该给它明确的初值,或者把它初始化位NULL:
int main(void)
{
int *p = NULL;
...
*p = 0;
...
}
空指针
把地址0转换成指针类型。
它的特殊之处在于,操作系统不会把任何数据保存在地址0及其附近,也不会把地址0-0xfff的页面映射到物理内存,所以任何对地址0的访问都会立刻导致段出错。*p=0;会导致段错误,就像放在眼前的炸弹一样很容易被找到。
void* 指针
ANSI在将C语言标准化时引入了void类型,void指针与其他类型的指针之间可以隐式转换,而不必用类型转换运算符。注意,只能定义void指针,而不能定义void型的变量,因为void指针和别的指针一样都占4个字节,而如果定义void型变量,编译器不知道该分配几个字节给变量。
void func(void *pv)
{
/* *pv = 'A' is illegal */
char *pchar = pv;
*pchar = 'A';
}
int main(void)
{
char c;
func(&c);
printf("%c\n", c);
指针类型的参数和返回值
#include <stdio.h>
int *swap(int *px, int *py)
{
int temp;
temp = *px;
*px = *py;
*py = temp;
return px;
}
int main(void)
{
int i = 10, j = 20;
int *p = swap(&i, &j);
printf("now i=%d j=%d *p=%d\n", i, j, *p);
return 0;
}
通过地址访问到内部变量,将内部变量进行交换。
指针和数组
int a[10];
int *pa = &a[0];
pa++;
在函数原型中,如果参数是数组,则等价于参数是指针的形式,例如:
void func(int a[10])
{
...
}
等价于
void func(int *a)
{
...
}
等价于
void func(int a[])
{
...
}
指针与const 限定符
- 如下
const int *a;
int const *a;
a是一个指向const int型的指针,a所指向的内存中的内容是不可改变的,所以(*a)++是不允许的,但a可以改写,所以a++是允许的。
2. 如下
int * const a;
a是一个指向int型的const指针,*a是可以改写的,但a不允许改写。
3.
int const* const a;
a是一个指向const int型的const指针,因此*a和a都不允许改写。
指向非const变量的指针或者非const变量的地址可以传给指向const变量的指针,编译器可以做隐式类型转换,例如:
char c = 'a';
const char *pc = &c;
但是,指向const变量的指针或者const变量的地址不可以传给指向非const变量的指针,以免投过后者意外改写了前者所指向的内存单元,例如对下面的代码编译器会报警告:
const char c = 'a';
char *pc = &c;
即使不用const限定符也能写出正确的程序,但良好的编程习惯应该尽可能多的使用const,因为:
1.const给读代码的人传达非常有用的信息。比如一个函数的参数是const char *,你在调用这个函数时就可以放心地传给它char *或const char *指针,而不必担心指针所指的内存单元被改写。
2.尽可能多地使用const限定符,把不该变的都声明成只读,这样可以依靠编译器检查程序中的Bug,防止意外改写数据。
3.const对编译器优化是一个有用的提示,编译器也许会把const变量优化成常量。
指针与结构体
首先定义一个结构体类型,然后定义这种类型的变量和指针:
struct unit {
char c;
int num;
};
struct unit u;
struct unit *p = &u;
要通过指针p访问结构体成员可以写成(*p).c和(*p).num,为了书写方便,C语言提供了->运算符,可以写成p->c和p->num。
指向指针的指针与指针数组
也就是二级指针:
int i;
int *pi = &i;
int **ppi = π
我们知道main函数的标准原型应该是int main(int argc,char *argv[]);argc是命令行参数的个数,而argv是一个指向指针的指针,为什么不是指针数组?因为前面讲过,函数原型中的[]表示指针而不表示数组,等价于char **argv.那为什么要写成char *argv[]而不写成char **argv?这样写给读代码的人提供了有用的信息,argv不是指向单个指针,而是指向一个指针数组的首元素。数组中每个元素都是char *指针,指向一个命令行参数字符串。
打开命令行参数
#include <stdio.h>
int main(int argc, char *argv[])
{
int i;
for(i = 0; i < argc; i++)
printf("argv[%d]=%s\n", i, argv[i]);
return 0;
}
编译:
$ gcc main.c
$ ./a.out a b c
argv[0]=./a.out
argv[1]=a
argv[2]=b
argv[3]=c
$ ln -s a.out printargv
$ ./printargv d e
argv[0]=./printargv
argv[1]=d
argv[2]=e
由于argv[4]是NULL,我们也可以这样循环遍历argv:
for(i=0; argv[i] != NULL; i++)
NULL标识着argv的结尾,这个循环碰到NULL就结束,因而不会访问越界,这种用法很形象地称为Sentinel,NULL就像一个哨兵守卫着数组的边界。
指向数组的指针与多维数组
指向数字的指针其实前面讲过的数组指针。
int (*a)[10];
int (*a)[10]可以拆分成
typedef int t[10];
t *a;
t代表由10个int组成的数组类型,a则是指向这种类型的指针。
也就是:
int a[10];
int (*pa)[10] = &a;
a是一个数组,在&a这个表达式中,数组名做左值,取整个数组的首地址赋给指针pa。注意,&a[0]表示数组a的首元素的首地址,而&a表示数组a的首地址,显然这两个地址的数值是相同的,但这两个表达式的类型是两种不同的指针类型,前者的类型是int ,而后者的类型是int()[10]。
函数类型和函数指针类型
在C语言中,函数也是一种类型,可以定义指向函数的指针。我们知道,指针变量的内存单元存放一个地址值,而函数指针存放的就是函数的入口地址(位于.text段)。下面看一个简单的例子:
函数指针:
#include <stdio.h>
void say_hello(const char *str)
{
printf("Hello %s\n", str);
}
int main(void)
{
void (*f)(const char *) = say_hello;
f("Guys");
return 0;
}
分析一下变量f的类型声明void(*f)(const char ),f首先跟号结合在一起,因此是一个指针。(*f)外面是一个函数原型的格式,参数是const char *,返回值是void,所以f是指向这种函数的指针。而say_hello的参数是const char *,返回值是void,正好是这种函数,因此f可以指向say_hello。注意,say_hello是一种函数类型,**而函数类型和数组类型类似,做右值使用时自动转换成函数指针类型,所以可以直接赋给f,当然也可以写成void (*f)(const char *) =&say_hello;**把函数say_hello先取地址再赋给f,就不需要自动类型转换了。
可以直接通过函数指针调用函数,如上面的f(“Guys”),也可以先用*f取出它所指的函数类型,再调用函数,即(*f)(“Guys”)。可以这么理解:函数调用运算符()要求操作数是函数指针,所以f(“Guys”)是最直接的写法,而say_hello(“Guys”)或(*f)(“Guys”)则是把函数类型自动转换成函数指针然后做函数调用。
文章来源:
https://blog.csdn.net/m0_58367586/article/details/128612310