一、指针数组和数组指针
1、指针数组
数组的每一个元素都是指针变量 ---本质:是一个数组(每个元素都是指针类型的数组)
形式: 数据类型 *指针数组名[数组长度];
注意:所有元素都必须是同种存储类型和指向相同数据类型,指针数组通常和指针的指针(双重指针)等价。
如: int r,b,c,d,e *a[5]={&r,&b,&c,&d,&e};
指针数组主要用于处理若干个字符串。“Sunday”代表的是首地址
如:char * name[] = {“Sunday”, “Monday”, “Tuesday”, “Wednesday”, “Thursday”, “Friday”, “Saturday”};
2.数组指针
- 指向由N个元素组成的二维数组 --本质:是一个指针
- 赋值方式:int a[5][10]; int (*pa)[10]=a ;或int (*pa)[10]; pa=a;
注意:()不能缺少。n必须与二维数组a[i][j]的j相同,数组指针通常和二维数组名等价。
访问方式:p[i][j] 或 *(p+i)[j] 或 *(*(p+i)+j)
//下标替换方法:p[i][j]
//替换行号:*(p+i)[j]
//函数指针常见方法:*(*(p+i)+j)
指针数组和数组指针的区别:
- 指针数组,如:
int *p[3]
指针数组:指针数组可以说成是指针的数组
,首先这个变量是一个数组。
其次,指针
修饰这个数组,意思是说这个数组的所有元素都是指针类型。
在 32 位系统中,指针占四个字节 - 数组指针,如:
int (*p)[3]
数组指针:数组指针可以说成是数组的指针
,首先这个变量是一个指针。
其次,数组
修饰这个指针,意思是说这个指针存放着一个数组的首地址,或者说这个指针指向一个数组的首地址
二、指针与字符串
字符指针:指向 char 型的指针变量 char *pa;
(1)让字符指针指向存放字符串的数组: char a[]=”abcd”;char *pa=a;
(2)让字符指针指向字符串常量: char *str; str=”abcdef”;
(3)定义字符指针时指向字符常量: char *str=”abcdefg”;
1.字符串指针数组问题
char str[10] = "hello"; // 定义一个长度为10的字符数组,存储字符串"hello"
char *str = "hello";// 使用 字符指针表示法:
在C语言中,char *str = "hello";
和 char str[] = "hello";
都可以用来表示字符串,但它们之间有一些区别:
- 存储方式:
char *str = "hello";
str
是一个指针,它指向一个字符串字面量(string literal)“hello”。字符串字面量通常存储在程序的只读内存区域(如常量区),因此不能修改。str
变量本身存储在栈上。char str[] = "hello";
:str
是一个字符数组,它的内容是字符串 “hello”。字符串的字符存储在栈上,因此可以修改。str
变量本身也存储在栈上。 - 可修改性:
char *str = "hello";
:由于str
指向的是只读内存区域
,所以不能修改字符串内容。例如,尝试修改字符串的操作(如str[0] = 'H';
)可能会导致程序崩溃。char str[] = "hello";
:由于str
是一个字符数组,存储在栈上,所以可以修改字符串内容。例如,可以执行str[0] = 'H';
操作。 sizeof
操作符的结果:char *str = "hello";
:使用sizeof(str)
时,将返回指针的大小(通常为4字节或8字节,取决于操作系统和编译器)。char str[] = "hello";
:使用sizeof(str)
时,将返回字符数组的大小,即6字节(包括字符串结尾的空字符’\0’)。
#include <stdio.h>
const int MAX = 4;
int main ()
{
const char *names[] = {
"Zara Ali",
"Hina Ali",
"Nuha Ali",
"Sara Ali",
};
int i = 0;
for ( i = 0; i < MAX; i++)
{
printf("Value of names[%d] = %s\n", i, names[i] );
}
return 0;
}
结果:
Value of names[0] = Zara Ali
Value of names[1] = Hina Ali
Value of names[2] = Nuha Ali
Value of names[3] = Sara Ali
解析:
由于
字符串
是以字符数组的形式存储的,以空字符('\0'
)作为结束标志。所以当定义一个字符串指针数组char *names[]
时,数组中的每个元素都是一个指向字符数组(字符串)的指针
。
当 访问names[i]
时,实际上是获取了指向字符串的指针。然而,C 语言
中的字符串处理函数(如printf
)通常会自动处理指针,它们会根据指针所指向的地址,逐个字符地读取字符串,直到遇到空字符('\0'
)为止。因此,当使用这些函数时,它们会显示字符串的内容,而不是指针的地址
实例:
#include <stdio.h>
void main()
{
char *names[] = {"Zara Ali","ASD"};
char **m;
m = names;
// m = names[0];
printf("Value of names = %p\n", names);
printf("Value of m = %p\n", m );
printf("Value of *m = %s\n", *m );
char *n;
n = *names;
// m = names[0];
printf("Value of n = %p\n", n );
printf("Value of n = %s\n", n );
printf("Value of *n = %c\n", *n );
}
输出结果:
Value of names = 0061FF10
Value of m = 0061FF10
Value of *m = Zara Ali
Value of n = 00405064
Value of n = Zara Ali
Value of *n = Z
分析:
char *names[] = {"Zara Ali", "ASD"};
定义了一个指针数组
,数组中的每个元素都是一个指向字符数组
(字符串)的指针。当使用 char *m;
时,定义了一个指向字符的指针
。所以,m
的类型是 char *
。
而 names
是一个指针数组
,它的类型是 char **
,因为它是一个指向指针的指针
。而数组名在大多数情况下会被解释为指向数组第一个元素的指针,所以 不能将 names
直接赋值给 m
,因为类型不同。
但是,你可以将 names
数组中的某个元素(即一个指向字符串的指针)赋值给 m,例如:m = names[0]; // 或者 m = *names;
这样,m 就指向了字符串 “Zara Ali”。
当使用 n = *names
时,相当于把字符串指针数组的第一个元素给了n,即"Zara Ali",当在使用 *n
时,就指向了字符串的第一个元素即字符:Z
三、双重指针
指向指针的指针
是一种多级间接寻址的形式,或者说是一个指针链
。通常,一个指针包含一个变量的地址。当我们定义一个指向指针的指针时,第一个指针包含了第二个指针的地址,第二个指针指向包含实际值的位置。
一个指向指针的指针变量必须如下声明,即在变量名前放置
两个星号
。例如,下面声明了一个指向 int 类型双重指针:int **var;
#include <stdio.h>
int main ()
{
int V;
int *Pt1;
int **Pt2;
V = 100;
/* 获取 V 的地址 */
Pt1 = &V;
/* 使用运算符 & 获取 Pt1 的地址 */
Pt2 = &Pt1;
/* 使用 pptr 获取值 */
printf("var = %d\n", V );
printf("Pt1 = %p\n", Pt1 );
printf("*Pt1 = %d\n", *Pt1 );
printf("Pt2 = %p\n", Pt2 );
printf("**Pt2 = %d\n", **Pt2);
return 0;
}
结果:
var = 100
Pt1 = 0x7ffee2d5e8d8
*Pt1 = 100
Pt2 = 0x7ffee2d5e8d0
**Pt2 = 100
四、野指针和空指针
指针变量也是变量,是变量就可以任意赋值,不要越界即可(32位为4字节,64位为8字节),但是,任意数值赋值给指针变量没有意义,因为这样的指针就成了野指针,此指针指向的区域是未知(操作系统不允许操作此指针指向的内存区域)。所以,野指针不会直接引发错误,操作野指针指向的内存区域才会出问题。
int a = 100;
int *p;
p = a; //把a的值赋值给指针变量p,p为野指针, ok,不会有问题,但没有意义
p = 0x12345678; //给指针变量p赋值,p为野指针, ok,不会有问题,但没有意义
*p = 1000; //操作野指针指向未知区域,内存出问题,err
但是,野指针和有效指针变量保存的都是数值,为了标志此指针变量没有指向任何变量(空闲可用),C语言中,可以把NULL赋值给此指针,这样就标志此指针为空指针,没有任何指针
int *p = NULL;
NULL是一个值为0的宏常量:
#define NULL ((void *)0)
五、万能指针void *
void *
指针可以指向任意变量的内存空间
void *p = NULL;
int a = 10;
p = (void *)&a; //指向变量时,最好转换为void *
//使用指针变量指向的内存时,转换为int *
*( (int *)p ) = 11;
printf("a = %d\n", a);
六、const修饰的指针变量
int a = 100;
int b = 200;
//指向常量的指针
//修饰*,指针指向内存区域不能修改,指针指向可以变
const int * p1 = &a;
// *p1 = 111 // err
p1 = &b; // ok
//指针常量
//修饰p1,指针指向不能变,指针指向的内存可以修改
int * const p2 = &a;
// p2 = &b; //err
*p2 = 333; //ok
在编辑程序时,指针作为函数参数,如果不想修改指针对应内存空间的值,需要使用const修饰指针数据类型
七、指针和函数
1、指针作为函数参数
下面的实例中,我们传递一个无符号的 long 型指针给函数,并在函数内改变这个值:
#include <stdio.h>
#include <time.h>
void getSeconds(unsigned long *par)
{
/* 获取当前的秒数 */
*par = time( NULL );
return;
}
int main ()
{
unsigned long sec;
getSeconds( &sec );
/* 输出实际值 */
printf("Number of seconds: %ld\n", sec );
return 0;
}
结果:
Number of seconds :1294450468
能接受指针作为参数的函数,也能接受数组作为参数,如下所示:
#include <stdio.h>
/* 函数声明 */
double getAverage(int *arr, int size);
int main ()
{
/* 带有 5 个元素的整型数组 */
int balance[5] = {1000, 2, 3, 17, 50};
double avg;
/* 传递一个指向数组的指针作为参数 */
avg = getAverage( balance, 5 ) ;
/* 输出返回值 */
printf("Average value is: %f\n", avg );
return 0;
}
double getAverage(int *arr, int size)
{
int i, sum = 0;
double avg;
for (i = 0; i < size; ++i)
{
sum += arr[i];
}
avg = (double)sum / size;
return avg;
}
结果:
Average value is: 214.40000
2、指针函数
- 指针函数:返回值的类型为指针类型的函数,本质是一个函数。
- 形式:函数类型 *函数名(参数表)
- 只要返回类型是指针类型的函数就是指针函数。
C 语言不支持在调用函数时返回局部变量的地址,除非定义局部变量为 static 变量
现在,让我们来看下面的函数,它会生成 10 个随机数,并使用表示指针的数组名(即第一个数组元素的地址)来返回它们,具体如下:
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
/* 要生成和返回随机数的函数 */
int * getRandom( )
{
static int r[10];
int i;
/* 设置种子 */
srand( (unsigned)time( NULL ) );
for ( i = 0; i < 10; ++i)
{
r[i] = rand();
printf("%d\n", r[i] );
}
return r;
}
/* 要调用上面定义函数的主函数 */
int main ()
{
/* 一个指向整数的指针 */
int *p;
int i;
p = getRandom();
for ( i = 0; i < 10; i++ )
{
printf("*(p + [%d]) : %d\n", i, *(p + i) );
}
return 0;
}
结果:
1523198053
1187214107
1108300978
430494959
1421301276
930971084
123250484
106932140
1604461820
149169022
*(p + [0]) : 1523198053
*(p + [1]) : 1187214107
*(p + [2]) : 1108300978
*(p + [3]) : 430494959
*(p + [4]) : 1421301276
*(p + [5]) : 930971084
*(p + [6]) : 123250484
*(p + [7]) : 106932140
*(p + [8]) : 1604461820
*(p + [9]) : 149169022
3、函数指针
函数指针: 指向函数的指针 --本质是指针
形式: 存储类型(数据类型) (* 函数指针变量名)(参数表); int (*p)(int x);
调用: (* 函数指针变量名) (参数表);
函数指针可以像一般函数一样,用于调用函数、传递参数。
```c
int (*fun_ptr)(int,int); //声明一个指向同样参数、返回值的函数指针类型
例:int fun(int x);//声明一个函数
int (*p)(int x);//声明一个函数指针变量
*p=&fun;或者p=fun; //这里不用带括号
#include <stdio.h>
int max(int x, int y)
{
return x > y ? x : y;
}
int main(void)
{
/* p 是函数指针 */
int (* p)(int, int) = & max; // &可以省略
int a, b, c, d;
printf("请输入三个数字:");
scanf("%d %d %d", & a, & b, & c);
/* 与直接调用函数等价,d = max(max(a, b), c) */
d = p(p(a, b), c);
printf("最大的数字是: %d\n", d);
return 0;
}
结果如下:
请输入三个数字:1 2 3
最大的数字是: 3
八、动态内存分配与指向它的指针变量
1.为什么存在动态内存分配?
int a = 20;//在栈空间开辟四个字节
char arr[10] = { 0 };//在栈空间开辟10字节的连续空间
这种开辟空间的方式的缺点:
- 空间开辟大小是固定的
- 数组在申明的时候,必须指定数组的长度,他所需要的内存在编译时分配
- 但是对于空间的需求,不仅需要如此,有时候我们需要的空间大小在程序运行的时候才能知道,那数组的编译时开辟空间的方式就不能满足了。这时候就需要动态内存开辟了。
2、动态内存函数介绍
(1)、malloc和free函数的介绍
动态开辟函数头文件均为 stdlib.h
void* malloc(size_t size) 申请内存
A、功能介绍:
向内存申请一块连续可用的空间,并返回指向这块空间的指针
B、参数介绍:
void *:因为开辟空间的类型不确定。如果开辟成功,则返回第一个字节地址(一个指向开辟好空间的指针),如果开辟失败,则返回一个NULL指针,所以是否开辟成功,最好要做一个检查,避免出现非法访问内存的问题
size:要开辟的字节数。如果他是0,malloc的行为是标准为定义的,取决于编译器,一般是不允许这么写的!
void free(void* ptr) 释放空间
A、功能介绍:
因为向内存动态申请了空间,你就应该对申请来的动态内存释放和回收,归还给操作系用,这就用到free函数了
B、参数介绍:
ptr:指向动态内存开辟的指针
C、注意:
1.如果参数ptr指向的空间不是动态内存开辟的,那么free的因为是未定义的(就是不允许这样的意思)
2.如果参数ptr是NULL指针,free函数什么都不做
例子如下:
#include<stdio.h>
#include<stdlib.h>
int main()
{
//比如我想开辟10个int类型的空间,如何动态开辟?
int* ptr = (int*)malloc(10 * sizeof(int));
//开辟完要判断malloc是否开辟成功
//防止访问NULL,非法访问内存
if (NULL != ptr)
{
int i = 0;
for (i = 0; i < 10; i++)
{
//其实你开辟完一段连续的内存空间
//ptr就可以当成数组名来用了
*(ptr + i) = 0;
//等价于ptr【i】= 0;
}
}
free(ptr);//释放ptr所指向的动态内存
ptr = NULL;
return 0;
}
上段代码有两个疑问:
一、为什么malloc要强制类型转换?
int* ptr = (int*)malloc(10 * sizeof(int));
因为mallo函数本来返回void*,你要开辟的为int类型的,所以你malloc就转为int的,这样int类型的指针解引用就可以访问4个字节,它的意义主要是在这里!
二、为什么要置ptr=NULL?
因为ptr指针指向的动态开辟的空间free函数已经释放了,ptr指针如果不置成NULL,还会指向那块空间,(ptr依然保存申请空间的地址)但是如果你后面又不小心访问了ptr所指向那块申请来的空间,它已经不属于你了,这是非法访问内存的行为,所以要把ptr置成NULL,防止你因为访问ptr所指向的那块动态开辟的空间而出错。
(2)calloc函数介绍
void* calloc(size_t num, size_t size)
A、功能介绍:
同样适用于动态内存开辟空间,与malloc差不多,只是有小差别而已
B、参数介绍:
num:要开辟的元素个数
size:一个元素多大,单位:字节
C、和malloc的区别:
1、参数不同,malloc只有一个参数而已
2、calloc会初始化内存每个元素为0,而malloc不会
相同在于都在内存的堆区上申请一块空间
calloc 和 malloc 的区别看下面的代码就懂了!
(3)realloc函数的介绍
void* realloc (void*ptr , size_t size)
A、功能介绍:
有时候我们会发现过去申请的空间太小或者过大了,而为了合理的使用内存,我们会对内存大小做一定的调整,realloc就可以做到,他让动态内存管理更加灵活。这个函数会在调整原内存空间大小的基础上,将原来内存中的数据移动到新的空间。
B、参数介绍:
1、ptr:要调整的内存地址
2、size:你要调整之后的新大小
3、返回void星:调整之后的内存起始地址
C、realloc调整空间存在的三种情况:
#include<stdio.h>
#include<stdlib.h>
int main()
{
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL)
{
//开辟失败perror函数会返回错误信息是什么
perror("main");
return 0;
}
//如果此时我想要20个int字节的空间,
//不只是10个字节空间,就要用realloc函数
int* ptr = (int*)realloc(p, 20 * sizeof(int));
if (ptr != NULL)
{
//开辟成功才能使p=ptr,因为如果失败它返回NULL
//p接收了NULL,它连原来的空间都会丢失!
p = ptr;
}
free(p);
p = NULL;
return 0;
}
九、指针数组做为main函数的形参
int main(int argc, char *argv [ ]);
-
main
函数是操作系统调用的,第一个参数标明argc
数组的成员数量,argv
数组的每个成员都是char *
类型 -
argv
是命令行参数的字符串数组 -
argc
代表命令行参数的数量,程序名字本身算一个参数
#include <stdio.h>
//argc: 传参数的个数(包含可执行程序)
//argv:指针数组,指向输入的参数
int main(int argc, char *argv[])
{
//指针数组,它是数组,每个元素都是指针
char *a[] = { "aaaaaaa", "bbbbbbbbbb", "ccccccc" };
int i = 0;
printf("argc = %d\n", argc);
for (i = 0; i < argc; i++)
{
printf("%s\n", argv[i]);
}
return 0;
}
总结:
- 指针的用法太多了,通过访问内存地址可以做很多事
- 又是励志的一天,将指针摸透,配合后面的只是应用起来,加油!工程师