12 指针

系列文章目录

提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加

例如:第一章 Python 机器学习入门之pandas的使用


提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录


前言

提示:这里可以添加本文要记录的大概内容:

例如:随着人工智能的不断发展,机器学习这门技术也越来越重要,很多人都开启了学习机器学习,本文就介绍了机器学习的基础内容。


提示:以下是本篇文章正文内容,下面案例可供参考

以下是对你指针学习笔记的全面补充与修正 。我保留了原有结构,更正了错误,澄清了模糊之处,并补充了缺失的重要知识点(如指针与 const 的更多细节、函数指针、指针与数组的更多区别等)。修改处已用 【修正】【补充】 标记,方便你对照。


一、基本概念

1.1 作用

  • 使程序简洁、紧凑、高效
  • 有效地表示复杂的数据结构(如链表、树、图)
  • 动态分配内存
  • 能直接访问硬件(如内存映射寄存器)
  • 能够方便地处理字符串
  • 得到多于一个的函数返回值(通过指针参数)

1.2 内存、地址、变量

  • 内存【修正】

    • 程序和数据永久存储在硬盘等非易失性存储器上,关机后不丢失。

    • 运行程序时,程序和数据必须从硬盘加载到内存,CPU 再从内存中读取指令和数据。

    • CPU 直接与内存交互,处理完成后将结果写回内存(或缓存)。

  • 内存地址【补充】

    • 内存以字节 为基本单位,每个字节都有一个唯一的编号,即内存地址

    • 32 位处理器有 32 根地址线,可寻址 2^32 字节 = 4 GB。

    • 64 位处理器通常支持 2^482^52 字节(实际未用满 64 位)。

      复制代码
      内存被划分成一个个小的单元格,每个单元格表示1个bit,只能存放0或1两种状态。以8bit为一组表示1个byte,并且将1byte大小作为内存操作的基本单元,对基本单元进行编址------内存地址。所有编号连起来就叫做内存的地址空间。
  • 变量

    • 变量是内存中某段区域的名字 ,用来存储数据。例如 int k = 58; 分配 4 个字节,k 代表这段空间,也代表其中存储的值。
  • 变量和地址

    • 变量 k 的地址是它占用的内存区域的首字节地址。例如,CPU为k分配4个字节0x0012FF02~0x0012FF050x0012FF02就作为变量k的地址。
    • 变量名在编译后被替换为地址,因此 CPU 只认地址,不认变量名。
  • 一切皆地址【补充概念】

    • 数据和代码都以二进制存储在内存中。操作系统通过内存权限区分:可读可执行的是代码段,可读可写的是数据段。当程序被加载到内存的过程中,就将各种内容加载到对应的内存块中。
    • CPU 访问内存时需要的是地址,而不是变量名和函数名!变量名和函数名只是地址的一种助记符,当源文件被编译和链接成可执行程序后,它们都会被替换成地址。编译和链接过程的一项重要任务就是找到这些名称所对应的地址。
    • 由于通过地址能找到所需的内存单元,可以说地址指向该内存单元。因此,将地址形象化的称为"指针"

1.3 指针和指针变量

定义

  • 指针 = 内存地址(一个无符号整数,是常量)。
  • 指针变量 = 存放地址的变量(内容可变)。

指针变量的一般形式

c 复制代码
<数据类型> *<指针变量名>;
int *p;   // p 是一个指针变量,可以存放 int 类型变量的地址
  • * 表示这是一个指针类型。
  • 数据类型决定了指针解引用时访问的字节数(步长)。例如 int * 步长 4(通常),char * 步长 1。
  • 一般数据类型要和这个地址中保存的数据的数据类型保持一致。

初始化

c 复制代码
<数据类型> *<指针变量名> = <地址量> ;
<数据类型> *<指针变量名> = NULL ;	

int a = -126;	////在内存中开辟一块内存空间
int *p = &a;   // & 取地址运算符,取出变量a的起始地址
  • 不能直接赋值一个普通整数(0 除外),因为类型不匹配。而是通过取地址符将某个变量的起始地址拿过来赋值。
  • 可以初始化为 NULL

1.4 指针的目标和解引用

  • 指针的目标 :指针所指向的内存区域中的数据 (即变量本身)。目标是一个左值(可以赋值)。

    c 复制代码
    int a = 126;
    int *p = &a;   // p 的目标是变量 a
  • 解引用 :通过 *p 访问指针指向的目标。*p 等价于 a。对*p进行加减乘除,也就是对a进行加减乘除,所以也称为"间接访问"

    c 复制代码
    int a = 126;
    int *p = &a;
    (*p)++;
  • 类型决定步长int *p 解引用访问 4 字节;char *p 访问 1 字节。

    c 复制代码
    int a = 0x11111111;
    
    int *p = &a;
    *p = 0x22222222;
    printf("%p\n",p);
    printf("%x\n",a);	//改变4字节
    
    char *p1 = (char *)&a;
    *p1 = 0x33333333;
    printf("%p\n",p);
    printf("%x\n",a);	//只改变了1字节
    
    short *p2 = (short *)&a;
    *p2 = 0x44444444;
    printf("%p\n",p);
    printf("%x\n",a);	//只改变了2字节

区别三种形式

c 复制代码
p    // 指针变量本身,内容是地址(右值)
*p   // 指针的目标,内容是数据(左值)
&p   // 指针变量的地址(右值,常量)
*(&p)  // 等价于 p, *和&两个运算符连用时,互相抵消

1.5 指针的赋值

  • 必须赋地址值 (如 &a、数组名、函数名、另一个同类型指针、NULL)。
  • 为什么普通整数不能直接赋给指针?
    虽然地址本质是整数,但 C 语言要求类型匹配。int *p = 100; 类型不匹配,编译器报错或警告。
    可以用强制转换 (int *)100,但通常危险(除非你知道 100 是有效地址)。
  • 赋 0 是例外int *p = 0; 表示空指针(NULL),标准允许整数字面量 0 可以隐式转换为任意指针类型,得到空指针。#define NULL ((void*)0)

常见赋值形式:注意类型相同

c 复制代码
double x=15, *px;
px = &x;                // ① 普通变量地址

float a, *px, *py;
px = &a;
py = px;                // ② 指针变量赋值

int a[20], *pa;
pa = a;                 // ③ 数组名赋值(等价 &a[0])

1.6 指针的大小

  • 32 位系统:指针变量占 4 字节

  • 64 位系统:指针变量占 8 字节

  • 与指针指向的类型无关,与计算机地址线数量有关。

  • 查看系统位数:uname -m(Linux)或 getconf LONG_BIT

    c 复制代码
    printf("%zu\n",sizeof(char *));
    printf("%zu\n",sizeof(short *));
    printf("%zu\n",sizeof(int *));
    printf("%zu\n",sizeof(float *));
    printf("%zu\n",sizeof(double *));
    复制代码
      对于32位的系统,假设有32根地址线,那么假设每根地址线在寻址的时候产生高电平和低电平。那么32根地址线产生的地址就是32个(1或者0)`2^32Byte = 4GB`,共 4G的空闲进行编址。同样的方法,那64位机器给64根地址线,`2^64Byte` 。

1.7 空指针

  • NULL 通常定义为 (void *)0 或直接 0
  • 空指针不指向任何有效内存,解引用会导致段错误(Segmentation Fault)。
  • 使用前应检查 if (p != NULL)
  • 为什么指向0地址处?2个原因。
    ① 指针如果没有给定初始值,值是不确定的
    ② 0地址是一个特殊地址,在一般的操作系统中都是不可被访问的,如果C语言程序员不按规矩(不检查是否等于NULL就去解引用)写代码直接去解引用就会触发段错误

1.8 野指针

  • 定义:指向非法或已释放内存的指针。
  • 危害:段错误(指向不可访问地址)、数据污染、难以调试的随机错误(指向可用且其他程序正在使用的地址)、错误被掩盖(指向可用且未被使用的空间,错误被掩盖)。
  • 规避方法
    1. 定义时初始化为 NULL
    2. 使用前确保指向有效内存(如 malloc、取地址)。
    3. 解引用前检查 if (p != NULL)
    4. 避免指针越界。
    5. 释放内存后将指针置为 NULL
    6. 不要返回局部变量的地址(函数返回后局部变量被销毁)。

正确示范:

复制代码
```c
	
	#include <stdio.h>

	int main(){
	    int a = 12,b = 23;
	    int t;
	    int *p = &a;
	    int *q = NULL;

	    printf("before: a = %d,b = %d\n",a,b);
	    q = &b;
	    if(p != NULL && q != NULL){
	        t = *p;
	        *p = *q;
	        *q = t;
	    }
	    printf("after: a = %d,b = %d\n",a,b);
	    return 0;
	}
```

错误示范:

复制代码
```c
int a = 10;
int *p = NULL;
if(a>10){
    int b = 20;
    p = &b;
    a += *p;
}
//(*p)++;	//野指针,出了if语句,b已经被销毁,p指向未定义空间,不该再使用了
p = NULL; 
```

二、指针运算

l 指针运算是以指针所存放的地址作为运算量而进行的

l 指针运算的实质就是地址的计算

2.1 算术运算

包括:指针 ± 整数、自增/自减、指针相减。

指针 ± 整数

  • p + n 的地址 = p 的地址 + n * sizeof(指向的类型)

  • p++ 移动一个步长(即 sizeof(*p))。

  • 不同数据类型的指针加减整数是有意义的 (分别移动各自的步长),但两个不同类型的指针相减 无意义(编译器报错)。

    c 复制代码
    //p+n
    #include <stdio.h>
    int main(){
        int a = 10;
        int *p = &a;
        printf("%p\n",p);
        printf("%p\n",p+1);
        char c = 'a';
        char *pc = &c;
        printf("%p\n",pc);
        printf("%p\n",pc+1);
        return 0;
    }

指针相减

  • p - q 结果是一个整数(ptrdiff_t),表示两个指针之间相隔的元素个数(要求指向同一数组或其后一位置)。

  • 两个指针相加是非法的。

  • 技巧:对于一段连续的空间,如果想要挨个字节访问,利用char *p指向首地址,p++逐个访问。同理,挨个整形访问,就用int *p。

    c 复制代码
    #include <stdio.h>
    
    int main(){
        int arr[] = {1,2,3,4,5,6,7,8};
        int *p = arr;
        
        printf("%d\n",*(p += 3));
        printf("%d\n",*(p -= 1));
    
        int *q = &arr[7];
        printf("%ld\n",q - p);
        printf("%ld\n",p - q);
        return 0;
    }

自增/自减的经典组合

c 复制代码
int a[] = {6, 8, 10};
int *p = a;
*p++;		//什么改变了,什么没变

++的优先级高于*,p先与++结合,后置运算返回原先的p,p解引用,最后p++。

*p++、(*p)++、++*p 、*++p 执行顺序,例:

c 复制代码
#include <stdio.h>

int main(){
    int arr[4] = {1,2,3,4};
    int *p = arr;

    printf("%d\n",*p++);	//1
    printf("%d\n",*p);	//2
    for(int i = 0; i < 4; i++){
        printf("%d ",arr[i]);	//1 2 3 4
    }
    putchar('\n');
		//初始化p指向数组的第一个元素 arr[0](值为 1)。
		//*p++:后缀 ++ 优先级高于 *,所以先执行 p++,但后缀自增返回的是 p 原来的值,然后 p 自增 1。因此 *p++ 等价于 *(p++),先取出 p 原来指向的值arr[0],第一个 printf 输出 1。然后 p 指向下一个元素 arr[1],第二个 printf 输出2
    p = arr;		//恢复原样
    printf("%d\n",(*p)++);	// 1
    printf("%d\n",*p);		// 2 
    for(int i = 0; i < 4; i++){
        printf("%d ",arr[i]);	// 2 2 3 4 
    }
    putchar('\n');
		//初始状态:p 指向 arr[0],arr[0] 的值为 1。
		//(*p)++:括号使 *p 先被取出,得到值 1,然后对该值(即 arr[0])执行后缀自增,返回(*p)原值,第一个printf打印 1 ,接着该值++,因此 arr[0] 变为 2。接着 printf("%d\n", *p);:此时 p 仍然指向 arr[0],而 arr[0] 已经变成了 2,所以输出 2。
    arr[0] = 1;	//恢复原样
    printf("%d\n",++*p);	// 2
    printf("%d\n",*p);		// 2
    for(int i = 0; i < 4; i++){
        printf("%d ",arr[i]);	// 2 2 3 4
    }
    putchar('\n');
		//初始时 p 指向 arr[0],arr[0] 的值为 1。
		//++*p:前缀++和*优先级相同,且是右结合,所以等价于 ++(*p)。先取 *p(值为 1),然后执行前缀自增,将 arr[0] 变为 2,并返回自增后的值 2。因此第一个 printf 输出 2。
		//此时 p 仍指向 arr[0],而 arr[0] 已经是 2,所以第二个 printf 输出 2
    arr[0] = 1;	//恢复原样
    printf("%d\n",*++p);	//2
    printf("%d\n",*p);		//2
    for(int i = 0; i < 4; i++){	
        printf("%d ",arr[i]);	// 1 2 3 4
    }
    putchar('\n');
	//p 初始指向 arr[0](值为 1)时:
	//*++p:前缀 ++ 与 * 优先级相同,右结合,等价于 *(++p)。先执行 ++p,使 p 指向 arr[1](值为 2),再解引用得到 2。第一个 printf 输出 2。
	//此时 p 已指向 arr[1],第二个 printf 输出 *p 的值,即 2。
    return 0;
}

以上内容换成 --也是同样逻辑,只是方向相反

总结

  • 后置 运算符(++,--)先返回变量原数据,是右值(不能赋值),表达式使用原数据执行完后,变量自加/减1(变量的加/减1操作在表达式之后,分号之前)

    后置(++,--)优先级高于*

  • 前置 运算符(++,--)变量先自加/减1(变量的加/减1操作在表达式之前),自加/减后将变量返回给表达式,是左值(可以赋值,但极少使用)

    前置(++,--)优先级与 * 相同,结合方式为右到左

    *p的结果是左值(可以赋值)。

  • *p++和(*p)++

    *p++;指针移动,原目标不变
    *p++ = 3;指针移动,原目标变为3
    (*p)++; 指针不移动,原目标 + 1
    (*p)++ = 3; 语法错误,(*p)++返回右值(后缀自增/减返回右值)

  • *++p和++*p

    *++p; 指针移动,原目标不变
    *++p = 3;指针移动,移动后指向的目标变为3
    ++*p;指针不移动,源目标 + 1
    ++*p = 3;指针不移动,++*p是左值,前缀自增/减返回左值。++*p = 3 等价于 ++(*p); *p = 3;最终*p被赋值为 3(先自增再覆盖)。

  • *p++ 中的 p++ 副作用(指针移动)会在整个表达式求值结束后的"序列点"(如语句结束的分号)之前完成,但表达式的值使用的是原指针。

  • *p++ = 3 或 ++*p = 3 虽然语法正确,但阅读困难。建议拆成多行代码,提高可维护性。

    c 复制代码
    int main()
    {
    int a[] = {5, 8, 7, 6, 2, 7, 3};
    int y,
    *p = &a[1];
    y = (*--p)++;
    printf("%d ", y);
    printf("%d", a[0]);
    }
    输出:5 6

大小端模式

大小端指的是在存储器中,存放数据的字节顺序

  • 小端:低字节存放在低地址(x86、ARM 常用)。
  • 大端:低字节存放在高地址(网络字节序、某些嵌入式)。
    unsigned int value = 0x12345678

写一段代码判断机器是大端模式还是小端模式

c 复制代码
include <stdio.h>

int main(){
    unsigned int num = 0x11223344;
    char* p = (char *)&num;

    printf("%x\n",num);
    printf("%x",*p);
    p++;
    printf("%x",*p);
    p++;
    printf("%x",*p);
    p++;
    printf("%x\n",*p);

    if(*p == 0x11){
        printf("little-endian\n");
    }else{
        printf("big-endian\n");
    }
    return 0;
}

2.2 指针关系运算

两指针之间的关系运算表示它们指向的地址位置之间的关系。

  • 可以比较两个指针(<, <=, >, >=, ==, !=),前提是指向同一数组或其后一位置。
  • 不能与指向数组第一个元素之前的位置比较(标准未定义)。

    P1可以与p2比较,不能与p3比较
  • 可与 NULL 比较。判断指针是否为空。

注意 :指针可以指向数组最后一个元素的下一个位置(用于循环终止),但不能解引用。

例:用指针实现逆序存放数组元素值

c 复制代码
#include <stdio.h>

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9};
    int size = sizeof(arr) / sizeof(int);
    int *p = arr,*q = &arr[size-1];
    int n;

    while(p < q){
        n = *p;
        *p = *q;
        *q = n;
        p++;
        q--;
    }
    p = arr;
    for(int i = 0; i < size; i++){
        printf("%d ",*p++);
    }
    putchar('\n');
    p = NULL;
    return 0;
}
c 复制代码
#include <stdio.h>

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9};
    int size = sizeof(arr) / sizeof(int);
    int *p = arr;
    int n;

    for(int i = 0; i < size/2; i++){
        n = *(p+i);
        *(p+i) = *(p+size-1 - i);
        *(p+size-1-i) = n;
    }
    p = arr;
    for(int i = 0; i < size; i++){
        printf("%d ",*p++);
    }
    putchar('\n');
    p = NULL;
    return 0;
}

三、指针与数组

数组指针是指数组在内存中的起始地址,数组元素的地址是指数组元素在内存中的起始地址

3.1 指针与一维数组

  • 数组名是一维数组的指针(起始地址),是地址常量 (右值),不能自增/自减。如:int a[10];
  • a[i]*(a+i)p[i]*(p+i) 等价(当 p = a 时)。
  • sizeof(a) 是整个数组的字节数;sizeof(p) 是指针本身的字节数(4/8)。
  • p[-1] 可能合法(如果 p 指向非首元素),但 a[-1] 永远越界(未定义行为)。

数组名与指针的区别

p=a的条件下,指针变量和数组在访问数组中元素时有相同的形式。

  • 含义不同
    • 数组名代表一个数组,存放相同类型的元素
    • 指针代表存储地址的变量
  • 使用不同
    • 数组名不是左值,不能出现在赋值左侧。
    • 指针是变量,可以赋值、自增。
    • 数组名在sizeof(a)&a时,a代表的对象是整个数组 ,其他情况a代表的对象是数组首元素地址
c 复制代码
include <stdio.h>

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9};
    int size = sizeof(arr) / sizeof(int);
    int *p = arr;

    printf("%d %p\n",*p,p);
    p = &arr[2]; //p = arr + 2;  p += 2;
    printf("%d %p\n",*p,p);

//  arr = arr + 2;

    printf("sizeof(arr) = %ld\n",sizeof(arr));  //40
    printf("sizeof(p) = %ld\n",sizeof(p));      //4 / 8
	
		//p[i]与arr[i]相等的前提是p = arr
    printf("%d %d %d %d\n",arr[1],*(arr+1),p[1],*(p+1));

		printf("%d\n",p[-1]);		//当p指向arr[0]之后时,合法,且不越界;当p指向arr[0],即p=arr时,越界
    printf("%d\n",arr[-1]);	//越界
    return 0;
}

3.2 二维数组

一级指针遍历二维数组

二维数组的元素连续存储,按行优先存

  • 将二维数组视为一维连续空间:int *p = &a[0][0]; 然后 p[i] 访问。
c 复制代码
#include <stdio.h>

int main() {
	int a[3][4] = {{1, 2, 3, 4}, {4, 5, 6, 8}, {2, 9, 3, 5}};
	int i, j;
	int n = sizeof(a) / sizeof(a[0]);
	int m  = sizeof(a[0]) / sizeof(int);
	int *p;

	for (i = 0; i < n; i++) {
		for (j = 0; j < m; j++) {
			//printf("%d ", a[i][j]);
			printf("%p ", &a[i][j]);	//观察地址
		}
		putchar('\n');
	}

	//一级指针遍历二维数组
	p = &a[0][0];
	for (i = 0; i < n * m; i++) {
		printf("%d %d\n", *(p+i), p[i]);
	}

	return 0;
}

数组指针(行指针)

把二维数组看作由多个一维数组构成。
int a[3][3],含有三个元素:a[0]、a[1]、a[2];元素a[0]、a[1]、a[2]都是一维数组名

  • 定义:int (*p)[列数]; 指向一维数组的指针。p叫做行指针变量。
  • 二维数组名是行指针(数组指针)常量,a 的类型为 int (*)[列数]
  • a+1 移动一整行。

补充:二维数组的几种访问方式

  • a[i][j]
  • *(a[i] + j)
  • *(*(a + i) + j)
  • (*(a+i))[j]

:行指针大小

c 复制代码
#include <stdio.h>

int main() {
	int a[3][4] = {{1, 2, 3, 4}, {4, 5, 6, 8}, {2, 9, 3, 5}};
	int (*p)[4] = a;

	printf("sizeof(a)=%ld\n", sizeof(a));
	printf("sizeof(a[0])=%ld\n", sizeof(a[0]));
	printf("sizeof(a[1])=%ld\n", sizeof(a[1]));
	printf("sizeof(a[2])=%ld\n", sizeof(a[2]));

	printf("%p %p\n", a, p);
	printf("%p %p\n", a + 1, p + 1);
	printf("%p %p\n", a + 2, p + 2);

	return 0;
}

:行指针遍历二维数组

c 复制代码
#include <stdio.h>

int main() {
    int a[3][4] = {{1, 2, 3, 4}, {4, 5, 6, 8}, {2, 9, 3, 5}};
    int i, j;
    int n = sizeof(a) / sizeof(a[0]);
    int m  = sizeof(a[0]) / sizeof(int);
    int (*p)[4];

    for (i = 0; i < n; i++) {
        for (j = 0; j < m; j++) {
            printf("%d ", a[i][j]);
            printf("%d ", *(a[i] + j));
            printf("%d ", *(*(a + i) + j));
        }
        putchar('\n');
    }

    p = a;
    for (i = 0; i < n; i++) {
        for(j = 0; j < m; j++){
            printf("%d \n", *(p[i] + j));
            printf("%d \n", *(*(p + i) + j));
            printf("%d \n", p[i][j]);

        }
    }

    return 0;
}

练习题

c 复制代码
int a[3][4] = { 0 };
printf("%ld\n", sizeof(a[0]));
printf("%ld\n", sizeof(*a));
printf("%ld\n", sizeof(*(a + 1)));
printf("%ld\n\n", sizeof(*(&a[0] + 1)));
printf("%ld\n", sizeof(a + 1));
printf("%ld\n", sizeof(&a[0] + 1));
printf("%ld\n\n", sizeof(a[0] + 1));
printf("%ld\n\n", sizeof(*(a[0] + 1)));
c 复制代码
int a[] = { 1, 2, 3, 4 };
printf("%ld\n", sizeof(&a));
printf("%ld\n", sizeof(*&a));
printf("%ld\n", sizeof(&a + 1));
c 复制代码
int a[5] = { 1, 2, 3, 4, 5 };
int* ptr = (int*)(&a + 1);
printf("%d,%d",*(a + 1),*(ptr - 1));
c 复制代码
int a[2][5] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
int* ptr1 = (int*)(&a + 1);
int* ptr2 = (int*)(*(a + 1));
printf("%d,%d",*(ptr1 - 1),*(ptr2 - 1));
c 复制代码
int a[5][5];
int(*p)[4];
p = a;
printf("%d\n", &p[2][1] - &a[2][1]);

3.2 字符指针与字符串

  • C使用字符数组来处理字符串

  • char数据类型的指针变量称为字符指针变量

  • 字符指针变量与字符数组有着密切关系,它也被用来处理字符串

  • 初始化字符指针是把内存中字符串的首地址赋予指针,并不是把该字符串复制到指针中

  • char *p = "Hello"; 指向字符串常量,不能修改 内容(*p = 'h' 错误)。

  • char s[] = "Hello"; 是字符数组,可以修改。

  • char *p = s;指向字符数组,可用修改(*p = 'h'

  • 字符串长度 strlen 不包括 '\0'

  • 字符指针可以遍历字符串,注意不要越界。

  • char s[] ="hello"char *s ="hello"有什么区别

    访问类似,但本质完全不同

    指针和数组名都表示首元素地址

    数组名是地址常量(右值)不能修改,指向的对象可以修改。指针可以被修改,指针指向的对象不能修改

:对于一个字符串,进行大小写字母转换

c 复制代码
#include <stdio.h>

int main() {
    char s[] = "How Are You! ABCDEFGHIJKLMNOPQRSTUVWXYZ abcdefghijklmnopqrstuvwxyz";
    char *p = s;
    
		printf("%s\n",s);
    while(*p != '\0'){
        if(*p >= 'A' && *p <= 'Z'){
            *p += 32;
        }else if(*p >= 'a' && *p <= 'z'){
            *p -= 32;
        }
        p++;
    }
    printf("%s\n",s);
    return 0;
}

:不利用任何字符串函数,编程实现字符串连接的功能。

c 复制代码
#include <stdio.h>

int main() {
    char s1[100] = "abcd";
    char *s2 = "EFGH";
    int i = 0;

    while(*s2 != '\0'){
        while(s1[i] != '\0'){
            i++;
        }
        s1[i] = *s2;
        s2++;
    }
    s1[i+1] = '\0';
    printf("%s\n",s1);
/*
    char s1[100] = "abcd";
    char *s2 = "EFGH";
    char *p = s1,*q = s2;

    while(*p != '\0'){
        p++;
    }
    while(*q != '\0'){
        *p = *q;
        p++;
        q++;
    }
    *p = '\0';

    printf("%s\n",s1);
*/
    return 0;
}

3.3 指针数组

由若干个具有相同数据类型的指针变量构成的集合

  • 定义:

    c 复制代码
    <数据类型> *<指针数组名>[<大小>];

    int *p[5]; 数组,每个元素是 int *。数组名p是指针数组的起始地址

  • 常用于存储多个字符串(char *s[] = {"abc","def"};)。

  • 指针数组名是二级指针常量(char ** 类型)。

  • 指针数组名加1,移动多少字节?32位移动4字节,64位移动8字节

c 复制代码
#include <stdio.h>
int main() {
	int a = 10, b = 20, c = 20;
	int * p[3];

	p[0] = &a;
	p[1] = &b;
	p[2] = &c;

	printf("a-b-c:%d %d %d\n", a, b, c);
	printf("%d %d %d\n", *p[0], *p[1], *p[2]);
	printf("%p %p %p\n", &a, &b, &c);
	printf("%p %p %p\n", p[0], p[1], p[2]);

	printf("sizeof:%ld\n", sizeof(p));	//12
	printf("%p %p\n", p, p + 1);

	return 0;
}
c 复制代码
#include <stdio.h>

int main() {
	int a[] = {5, 8, 2, 5, 9, 10};
	int * p[6];
	int i;

	for (i = 0; i < sizeof(a) / sizeof(int); i++) {
		p[i] = &a[i];
	}

	for (i = 0; i < sizeof(a) / sizeof(int); i++) {
		printf("%d\n", *p[i]);
	}

	return 0;
}
c 复制代码
#include <stdio.h>

int main() {
	char * s[] = {"BeiJing", "ShangHai", "GuangZhou"};
	int i;
	for (i = 0; i < sizeof(s) / sizeof(char *); i++) {
		printf("%s\n", s[i]);
	}
	return 0;
}

指针数组与二维数组

• 声明一个指针数组:

c 复制代码
int * p[2] ,a[2][3];

• 把一维数组a[0]a[1]的首地址分别赋予指针变量数组的数组元数p[0]p[1]

c 复制代码
p[0]=a[0];
p[1]=a[1];


指针数组遍历二维数组

c 复制代码
#include <stdio.h>

int main() {
	int a[2][3] = {{3, 5, 1}, {3, 6, 9}};
	int * p[2];
	int i, j;
	
	p[0] = a[0];//&a[0][0];
  p[1] = a[1];//&a[1][0];

	for	(i = 0; i < 2; i++) {
		for (j = 0; j < 3; j++) {
			//printf("%d ", a[i][j]);
			printf("%d %d ", *(p[i] + j), p[i][j]);
		}
		printf("\n");
	}
				
	return 0;
}

数组指针 vs 指针数组

  • int *p[5]
    • 指针数组,本质是数组(元素是指针的一维数组)
    • 指针数组经常结合二维数组使用,存储每行首元素的地址
  • int (*p)[5]
    • 数组指针,本质是指针(指向有5个int的一维数组)
    • 一维数组名取地址,或者二维数组名,都可以赋给它

四、多级指针

把一个指向指针变量的指针变量,称为多级指针变量

• 一级指针变量:指向处理数据的指针变量

• 二级指针变量:指向一级指针变量的指针变量

二级指针变量的说明形式如下

c 复制代码
<数据类型> ** <指针名> ;
  • int **p:指向 int * 的指针。
  • 多级指针常用于函数参数(传递指针的地址)或动态二维数组。
  • 运算步长:p+1 移动一个 int * 的大小(4/8)。
c 复制代码
#include <stdio.h>

int main() {

   int a = 10;
   int *p = &a;
   int **q = &p;

   printf("%d %p %p\n",a,p,q);
   printf("%p %p %p\n",&a,&p,&q);
   printf("%ld %ld %ld\n",sizeof(a),sizeof(p),sizeof(q));
   printf("%d %d %d\n",a,*p,**q);
   return 0;
}

多级指针的运算

  • 指针变量加1,是向地址大的方向移动一个目标数据
  • 多级指针运算也是以其目标变量为单位进行偏移
    int **p;p+1移动一个int *变量所占的内存空间(4/8)
    int ***p,p+1移动一个int **所占的内存空间(4/8)
c 复制代码
#include <stdio.h>

int main() {

    int a = 10;
    int *p = &a;
    int **q = &p;
    int ***g = &q;

    printf("int *:%p %p\n",p,p+1);
    printf("int **:%p %p\n",q,q+1);
    printf("int ***:%p %p\n",g,g+1);
    return 0;
}
int *:0x7ffc3d64d20c 0x7ffc3d64d210
int **:0x7ffc3d64d210 0x7ffc3d64d218
int ***:0x7ffc3d64d218 0x7ffc3d64d220

多级指针与数组

指针数组也可以用另外一个指针来处理。

例如:有一个一维字符指针数组ps[5],

c 复制代码
char *ps[5]= { "Beijing city", ......"London city" } ;

定义另一个指针变量pps,并且把指针数组的首地址赋予指针pps

c 复制代码
char *ps[5]={......}; char ** pps = ps;

由于ps是常量,不能自增/减,利用一个二级指针指向,达到灵活运用的效果

c 复制代码
#include <stdio.h>
int main() {
    char *ps[5] = {"Beijing","Moacow","NewYork","Paris","London"};
    char **pps = ps;

    printf(" %s\n %s\n %s\n\n",ps[1],pps[1],*(pps + 1));
    for(int i = 0; i < 5; i++){
        printf("%s\n",*pps++);
    }
    return 0;
}
c 复制代码
#include <stdio.h>

int main() {

    int a[] = {1,2,3,4,5};
    int n = sizeof(a) / sizeof(int);
    int *p[5];
    int **q;

    for(int i = 0; i < n; i++){
        p[i] = &a[i];
    }
    for(int i = 0; i < n; i++ ){
        printf("%d %d\n",a[i],*p[i]);
    }
    q = p;  //&p[0]
    printf("%d %d %d %d\n",a[1],*p[1],**(q+1),*q[1]);
    return 0;
}


补充:二级指针与指针数组的关系

c 复制代码
char *arr[] = {"a","b","c"};
char **p = arr;   // p 指向第一个指针元素

void 指针

万能指针 其实就是void *类型的指针,而void *指针一般被称为通用指针 或叫泛指针

一般形式为:

c 复制代码
void * <指针变量名称> ;
  • void *p 可以指向任何类型,但不能直接解引用或算术运算(因为不知道步长)。
  • 使用前必须被初始化,使用时需要强制转换:*(int *)p
  • 典型应用:malloc 返回值、函数参数(如 qsortpthread_create)。增大函数的适用范围
c 复制代码
#include <stdio.h>
int main() {
	int a = 10;
	float b = 3.14;
	char c = 'c';
	void * p;
	
	p = &a;
	printf("int:%d %d\n", a, *(int *)p);
	p = &b;
	printf("float:%f %f\n", b, *(float *)p);
	p = &c;
	printf("char:%c %c\n", c, *(char *)p);
	return 0;
}

const 指针

  • 指向常量的指针const int *p;int const *p;
    不能通过 *p 修改目标,但 p 本身可以指向别处。
  • 指针常量int * const p;
    p 的指向不可变,但可以通过 *p 修改目标。
  • 指向常量的指针常量const int * const p;
    两者都不可变。

注意const 放在 * 左边修饰指向的内容,右边修饰指针本身。

main 函数参数

c 复制代码
int main(int argc, char *argv[])  // 或 char **argv
  • argc:命令行参数个数(包括程序名)。
  • argv[0]:程序名。
  • argv[1]argv[argc-1] 是命令行参数(命令行中各字符串的首地址)。
  • argv[argc]NULL

1. 函数指针

  • 定义:int (*func_ptr)(int, int);

  • 指向函数的指针,可用于回调函数、动态调用。

  • 示例:

    c 复制代码
    int add(int a, int b) { return a+b; }
    int (*p)(int,int) = add;
    int c = p(2,3);   // 调用

2. 动态内存分配

  • malloc, calloc, realloc, free
  • 必须检查返回值是否为 NULL
  • 避免内存泄漏:配对 free

3. 指针的指针与二维数组的动态分配

c 复制代码
int **matrix = (int**)malloc(rows * sizeof(int*));
for (int i = 0; i < rows; i++)
    matrix[i] = (int*)malloc(cols * sizeof(int));

4. 常见错误与陷阱

  • 使用未初始化的指针。
  • 返回局部变量的地址。
  • 指针越界。
  • 忘记分配内存就直接解引用。
  • 内存泄漏(丢失 free 的机会)。
相关推荐
charlie1145141912 小时前
嵌入式现代C++工程实践——第10篇:HAL_GPIO_Init —— 把引脚配置告诉芯片的仪式
开发语言·c++·stm32·单片机·c
call me by ur name2 小时前
ERNIE 5.0 Technical Report论文解读
android·开发语言·人工智能·机器学习·ai·kotlin
dog2502 小时前
细看高维空间中距离度量失效
开发语言·php
码云数智-大飞2 小时前
Rust的所有权模型如何消除内存安全问题?与C++的RAII有何异同?
开发语言
如意猴2 小时前
【前端】002--怎样制作一个简历界面?
开发语言·前端·javascript
夜珀2 小时前
OpenTiny NEXT 从入门到精通·第 6 篇
开发语言·前端框架
爱编码的小八嘎2 小时前
C语言完美演绎7-11
c语言
爱编码的小八嘎2 小时前
C语言完美演绎7-9
c语言
仍然.2 小时前
多线程---CAS,JUC组件和线程安全的集合类
java·开发语言