当前位置: 首页 > news >正文

【C语言】第八期——指针、二维数组与字符串

目录

1 初始指针

2 获取变量的地址

3 定义指针变量、取地址、取值

3.1 定义指针变量

3.2 取地址、取值

4 对指针变量进行读写操作

5 指针变量作为函数参数

6 数组与指针

6.1 指针元素指向数组

6.2 指针加减运算(了解)

6.2.1 指针加减具体数字 

6.2.2 指针加减指针

6.3 数组名与数组元素首地址的关系

6.4 数组作为函数参数

7 二维数组以及字符串与指针

7.1 二维数组的定义方法

7.1.1 定义一个二维数组 

7.1.2 访问二维数组

7.2 定义字符串的几种方法

7.3 字符串数组

7.4 strcat 连接字符串

7.5 strcpy 字符串复制(拷贝)函数


1 初始指针

通过前面的教程我们知道变量是用来存储数据的,变量的本质是给存储数据的内存地址起了一个好记的别名

比如我们定义了一个变量 int a= 10 ,这个时候可以直接通过a这个变量来读取内存中保存的10 这个值,在计算机底层a这个变量其实对应了一个内存地址

指针也是一个变量,但它是一种特殊的变量,它存储的数据不是一个普通的值,而是另一个变量的内存地址

每一个变量都有一个内存位置,每一个内存位置都定义了可使用连字号(&)运算符访问的地址,它表 示了在内存中的一个地址

刚开始学C语言的指针操作,我们只需要记住两个符号 :&(取地址) 和 *(根据地址取值 /定义指针变量)


2 获取变量的地址

每个变量在运行时都拥有一个地址,这个地址代表变量在内存中的位置。C语言中使用&字符放在变量 前面对变量进行取地址操作

#include <stdio.h>
int main(void)
{int a = 10;printf("a的地址是:%p\n", &a);return 0;
}

3 定义指针变量、取地址、取值

3.1 定义指针变量

指针是一个变量,其值为另一个变量的地址,即,内存位置的直接地址。就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为:

type *var-name;

在这里,type 是指针的基类型,它必须是一个有效的 C 数据类型,var-name 是指针变量的名称。用 来声明指针的星号 * 与乘法中使用的星号是相同的。但是,在这个语句中,星号是用来指定一个变量是 指针

以下是有效的指针声明:

int* ip;    //一个整型的指针
double* dp; //一个 double 型的指针
float* fp;  //一个浮点型的指针
char* ch    //一个字符型的指针

所有指针的值都是一个地址,不管是整型、浮点型、字符型,还是其他的数据类型,都是一样的,都是 一个代表内存地址的长的十六进制数。不同数据类型的指针之间唯一的不同是,指针所指向的变量或常量的数据类型不同


3.2 取地址、取值

#include <stdio.h>
int main(void)
{// 定义一个int变量aint a = 10;printf("a的地址是:%p\n", &a);// 定义int类型的指针变量pint* p_a = &a;printf("指针p_a的值:%p\n", p_a);printf("指针p_a的地址:%p\n", &p_a);printf("a的值:%d\n", a);printf("根据指针p_a的值去内存取值得到的结果为:%d", *p_a);return 0;
}

总结:

  • 取地址操作符&和取值操作符是一对互补操作符, & 取出地址, 根据地址取出地址指向的值
  • 对变量进行取地址(&)操作,可以获得这个变量的地址
  • 指针变量的值是地址
  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值

4 对指针变量进行读写操作

#include <stdio.h>
int main(void)
{int a = 10;int b = 20;int* p1, * p2; // 定义指针变量 p1、 p2p1 = &a;// p1 指向变量 ap2 = p1;// p2 指向变量 aprintf("&a=%p p1=%p p2=%p\n", &a, p1, p2);*p1 = 20;printf("a的值%d,取指针得到的值%d", a, *p1);return 0;
}

运行结果:

&a=000000612A17FA74 p1=000000612A17FA74 p2=000000612A17FA74
a的值20,取指针得到的值20


5 指针变量作为函数参数

在C语言中,函数参数不仅可以是字符型、整型、浮点型等,还可以是指针类型,作用是将变量地址传递给函数形参

#include <stdio.h> 
void modify1(int *c) 
{*c = 20;
}
int main(void) {int a = 10;modify1(&a);printf("%d", a);return 0;
}

6 数组与指针

6.1 指针元素指向数组

数组本质上是一片连续的内存空间,每个数组元素都对应一块独立的内存空间,它们都有相应的地址。

因此,指针变量既然可以指向变量,也就可以指向数组元素

数组的构成本质上是:数组名[偏移量]

数组名就代表第一个元素的地址,偏移量就是在此基础上偏移

数组的元素本质上完全可以当做是单独的变量对待,只不过没有名称而已

int a[5]={1,2,3,4,5}; //定义长度为5的int数组
int *p_a=&a[0]; //定义指向int变量的指针变量p_a,把a数组第0个元素地址赋给指针变量p_a

注意:&a[0]等价于&(a[0]),由于[ ]运算符比取地址运算符&优先级高,因此&(a[0])中的小括号可以省略,简写为:&a[0]

在计算机中内存的最小单位是字节,每个字节都对应一个地址

如果一个变量占用多个字节,就会占用 多个内存地址

例如: char 类型变量占1字节就对应1个地址,short 类型变量占2字节对应2个地址,int 类型变量占4 字节对应4个地址.....·其他类型依次类推

同理,数组元素类型不同占用的内存地址也不同

下面通过例子来验证以上分析

#include<stdio.h>
int main(void)
{char c[5];short s[5];int i;for (i = 0; i < 5; i++){printf("&c[%d]=%p ", i, &c[i]);printf("&s[%d]=%p \n", i, &s[i]);}return 0;
}

运行结果:

&c[0]=000000000061FE17 &s[0]=000000000061FE0C

&c[1]=000000000061FE18 &s[1]=000000000061FE0E

&c[2]=000000000061FE19 &s[2]=000000000061FE10

&c[3]=000000000061FE1A &s[3]=000000000061FE12

&c[4]=000000000061FE1B &s[4]=000000000061FE14

说明:不同设备上面输出的地址可能不一样的,数组中每个元素地址都是连续的


6.2 指针加减运算(了解)

6.2.1 指针加减具体数字 

指针本质上就是内存地址,在32 位操作系统下,内存地址是 4 字节的整数。既然是整数就可以进行 加、减、乘、除等算术运算。不过需要注意的是,在 C 语言中一般只讨论指针加、减运算,乘、除等其 他算术运算都没有意义

在实际开发中,指针加、减运算多用于数组(或连续内存空间)。当指针变量p 指向数组元素时,p+1 表 示指向下一个数组元素,p-1 表示指向上一个数组元素

#include <stdio.h>
int main(void)
{int a[3] = { 1, 2, 3 };int* p = &a[0];// 指针 p 指向 a[0]printf("%p %d\n", p, *p); // 输出 a[0] 的地址 和 a[0] 的值p = p + 1;// p 加 1printf("%p %d\n", p, *p); // 输出 a[1] 的地址 和 a[1] 的值return 0;
}

运行结果:

000000811557F968 1
000000811557F96C 2

注意:实现指针加减的时候需要注意越界问题


6.2.2 指针加减指针

在C语言中,两个指针相加是没有意义的,而两个指针相减却有特殊的意义,不过只有当两个指针都指向同一数组中的元素时才有意义

一个数组中的元素时,对它们进行减法运算,得到的结果是这两个指针所指向元素之间相隔的元素个数 (不是之间有的个数,之间有的元素数要在此基础上减一),而不是它们地址差值的字节数。其计算方式是用两个指针的地址差值除以指针所指向数据类型的大小

用公式表示为:(指针2的地址 - 指针1的地址) / sizeof(指针所指向的数据类型)


6.3 数组名与数组元素首地址的关系

C语言中,数组名与数组首元素地址等价。也就是说,在程序中,输出数组名与输出数组首元素地址是相同的

#include <stdio.h>
int main(void)
{int num[5];printf("%p\n", num);     // 输出数组名printf("%p\n", &num[0]); // 输出数组首元素地址return 0;
}

输出结果:

000000000061FE00

000000000061FE00

我们就可以通过把数组名复制给指针变量,来把数组首地址给指针变量

#include <stdio.h>int main(void){int num[5] = {1, 2, 3, 4, 5};int *p_num = num; // 把num的首地址赋值给指针变量printf("p_num指针的值=%p \n p_num对应数组元素的值%d", p_num, *p_num);return 0;}

我们也可以通过p_num来访问数组里面的每个元素

 #include <stdio.h>int main(void){int num[5] = {1, 2, 3, 4, 5};int *p_num = num; // 把num的首地址赋值给指针变量printf("p_num指针的值=%p ,num的地址是=%p  p_num对应数组元素的值%d\n", p_num, num, 
*p_num);printf("p_num访问第一个元素=%d\n", p_num[0]);printf("p_num访问第二个元素=%d\n", p_num[1]);return 0;}

运行结果:

p_num指针的值=000000000061FE00 ,num的地址是=000000000061FE00  p_num对应数组元素的值1

p_num访问第一个元素=1 p_num访问第二个元素=2


6.4 数组作为函数参数

函数参数不仅可以是变量,也可以是数组,数组作函数参数的作用是将数组首元素地址传给函数作形参。 在 C 语言中,数组作函数参数时,是没有副本机制的,只能传递地址。也可以认为,数组作函数参数时,会退化为指针

#include <stdio.h>
void getSize(int nums[10]) // 定义 getSize 函数
{int size = sizeof(nums); // 计算数组 nums 的总字节数printf("指针 size=%d\n", size);
}
int main(void)
{int nums[10] = { 1, 2, 3, 4, 5 };int size = sizeof(nums); // 计算数组 nums 的总字节数printf("外部 size=%d\n", size);getSize(nums); // 调用 getSize 函数return 0;
}

 运行结果:

外部 size=40

指针 size=8

所以直接把函数的形参的类型设置为指针,来接收数组的第一个地址: 

#include <stdio.h>
void getSize(int* nums) // 定义 getSize 函数
{int size = sizeof(nums); // 计算数组 nums 的总字节数printf("指针 size=%d\n", size);
}
int main(void)
{int nums[10] = { 1, 2, 3, 4, 5 };int size = sizeof(nums); // 计算数组 nums 的总字节数printf("外部 size=%d\n", size);getSize(nums); // 调用 getSize 函数return 0;
}

运行结果:

外部 size=40

指针 size=8

分析:

32位系统中指针变量占4个字节,64位系统中指针变量占8个字节,所以getSize中num的字节数是8。在 C 语言中,数组作函数参数时,是没有副本机制的,只能传递地址,所以 的形参 int *nums应该是指针变量, void getSize方法 getSize(nums) 传入的nums并不是数组的副本,而是数组首元素的地址 

注意:因为没有副本机制,所以如果在函数内部修改数组内容,数组的内容会直接被修改,而不是修改副本

#include <stdio.h>
void getSize(int* nums) // 定义 getSize 函数
{nums[0] = 3;
}
int main(void)
{int nums[10] = { 1, 2, 3, 4, 5 };printf("%d\n", nums[0]);    //加\n是为了换行getSize(nums); // 调用 getSize 函数printf("%d", nums[0]);return 0;
}

练习:封装比较数组最大值函数

#include <stdio.h>
int getMax(int* nums, int length) // 定义函数 getMax
{int i;int max = nums[0]; // 默认 nums[0] 为最大值for (i = 1; i < length; i++) // 下标从 1 开始遍历{if (max < nums[i]) // 比较大小{max = nums[i]; // 覆盖最大值}}return max; // 返回最大值
}int main(void)
{int nums[10] = { 11, 22, 3, 24, 15, 8, 99, 21, 35, 0 };int length = sizeof(nums) / sizeof(nums[0]); // 计算数组长度int max = getMax(nums, length); // 返回最大值printf(" 最大值为 :%d\n", max);return 0;
}

7 二维数组以及字符串与指针

7.1 二维数组的定义方法

C语言中二维数组和一维数组类似,简单理解就是:二维数组由多个一维数组构成

7.1.1 定义一个二维数组 

type arrayName[x][y];

例:

int a[3][4]={{0,1,2,3},{4,5,6,7},{8,9,10,11}};

内部嵌套的括号是可选的,下面的初始化与上面是等同的:

int a[3][4] = {0,1,2,3,4,5,6,7,8,9,10,11};

7.1.2 访问二维数组

#include <stdio.h>
#include <string.h>
void main(void)
{int a[3][4] = { {1, 3, 5, 7}, {9, 11, 13, 15}, {17, 19, 21, 23} };printf("a[0][2]=%d", a[0][2]);
}

运行代码:

a[0][2]=5


7.2 定义字符串的几种方法

我们已经学习的定义方法:

#include <stdio.h>
int main(void)
{char chs1[] = { 'i', 't', 'y', 'i', 'n', 'g', '\0' };char chs2[] = "itying";printf("%s\n", chs1);// 以 %s 格式输出 strprintf("%c\n", chs1[2]); // 以 %c 格式输出一个字符printf("%s\n", chs2);  // 以 %s 格式输出 strprintf("%c", chs2[2]); // 以 %c 格式输出一个字符return 0;
}

我们也可以使用字符指针引用字符串:

#include <stdio.h>
int main(void)
{char chs1[] = "itying";char* str1 = chs1;printf("%s\n", str1);  // 以 %s 格式输出 strprintf("%c\n", str1[2]);   // 以 %c 格式输出一个字符
//---------------------------------------------------------------------------char* str2 = "this is str";printf("%s\n", str2);  // 以 %s 格式输出 strprintf("%c\n", str2[2]); // 以 %c 格式输出一个字符return 0;
}

通过上面示例我们可以通过三种方法定义字符串了

char chs1[] = {'i', 't', 'y', 'i', 'n', 'g', '\0'};
char chs2[] = "itying";
char *chs3 = "itying";

7.3 字符串数组

定义了一个指针数组,即数组中的每个元素都是一个 char 类型的指针:

char *arr[] 

例:

char *s[]={ "马总", "张总", "王麻子" };

当使用 { "马总", "张总", "王麻子" } 对这个数组进行初始化时,会把每个字符串常量的首字符地址分别赋给数组 arr 的各个元素:

  • arr[0] 被赋值为字符串 "马总" 的首字符地址,通过这个指针,就可以访问该字符串的所有字符
  • arr[1] 被赋值为字符串 "张总" 的首字符地址
  • arr[2] 被赋值为字符串 "王麻子" 的首字符地址

    在实际处理中文时,通常会使用多字节字符编码,比如常见的 UTF - 8 编码或 GBK 编码

    UTF - 8 是一种变长编码,一个中文字符通常用 3 个字节来表示。对于字符串 "张总",“张” 字在 UTF - 8 编码下会占用 3 个字节的存储空间,arr[1] 指向的就是这 3 个字节中第一个字节的存储地址


    7.4 strcat 连接字符串

    前面我们已经学习了 strlen 计算字符串数组有效长度、sprintf 字符串格式化函数、字符串拼接、整型转换成字符串等字符串知识,详细见【C语言】第七期——字符数组、字符串、类型转换

    现在我们继续学习相关知识

    原型:

    char *strcat(char *dest, const char *src);    //使用前需先引入头文件<string.h>

    参数:

    • dest:目标字符串的指针,拼接后的结果将存储在这个字符串中。dest 必须有足够的空间来容纳 src 字符串的内容以及拼接后的结果
    • src:源字符串的指针,它将被追加到 dest 字符串的末尾。src 字符串本身不会被修改

    返回值:

    函数返回一个指向目标字符串 dest 的指针

    工作原理:

    strcat 函数会从 dest 字符串的第一个空字符 '\0' 开始,将 src 字符串的内容复制到 dest 的末尾,直到遇到 src 字符串的空字符 '\0' 为止,最后,dest 字符串会以空字符 '\0' 结尾

    #include <stdio.h>
    #include <string.h>int main() {// 定义目标字符串和源字符串char dest[50] = "Hello, ";const char src[] = "World!";// 使用 strcat 函数将 src 追加到 dest 的末尾strcat(dest, src);// 输出结果printf("拼接后的字符串: %s\n", dest);return 0;
    }

    注意事项:

    • 内存空间:dest 数组必须有足够的空间来容纳 src 字符串的内容,否则会导致缓冲区溢出,这可能会引发程序崩溃或安全漏洞
    • 空字符:dest 字符串必须以空字符 '\0' 结尾,否则 strcat 函数无法确定从哪里开始追加
    • 字符串重叠:dest 和 src 所指向的字符串不能重叠,否则会导致未定义行为

    7.5 strcpy 字符串复制(拷贝)函数

    原型:

    char *strcpy(char *dest, const char *src);

    参数:

    • dest:指向目标字符数组的指针,用于存储复制后的字符串。目标数组必须有足够的空间来容纳源字符串及其终止的空字符 '\0'
    • src:指向源字符串的指针,即要被复制的字符串。该参数被声明为 const,表示在函数内部不会修改源字符串

    返回值:

    strcpy 函数返回指向目标字符数组 dest 的指针

    工作原理:

    strcpy 函数会将 src 指向的字符串(包括终止的空字符 '\0')复制到 dest 指向的字符数组中

    复制过程会一直进行,直到遇到源字符串的终止空字符 '\0',并将该空字符也复制到目标数组中

    #include <stdio.h>
    #include <string.h>int main() {// 定义源字符串char src[] = "Hello, World!";// 定义目标字符数组,确保有足够的空间char dest[20];// 使用 strcpy 函数复制字符串strcpy(dest, src);// 输出复制后的字符串printf("Copied string: %s\n", dest);return 0;
    }

    注意事项:

    • 缓冲区溢出风险:使用 strcpy 时需要确保目标数组有足够的空间来容纳源字符串及其终止空字符,否则可能会导致缓冲区溢出,引发未定义行为,为了避免这种风险,可以使用更安全的 strncpy 函数
    • 目标数组的初始化:在使用 strcpy 之前,不需要对目标数组进行初始化,因为 strcpy 会覆盖目标数组中的原有内容

    http://www.mrgr.cn/news/92123.html

    相关文章:

  • 关于order by的sql注入实验
  • 【Python LeetCode 专题】树
  • Ubuntu 22.04 一键部署MinerU1.1.0
  • dockerfile构建haproxy
  • superset
  • 本地部署AI模型 --- DeepSeek(一)
  • Day9 25/2/22 SAT
  • 玩机日记 13.5 群晖部署AList并配置SSL,安装opkg,使用rclone挂载到本地
  • 记录一次 ALG 的处理过程
  • 数据结构绪论
  • 关于Transparent native-to-ascii conversion
  • Spring Boot3+Vue2极速整合:10分钟搭建DeepSeek AI对话系统
  • Android KMP初探
  • Java 大视界 -- 总结与展望:Java 大数据领域的新征程与无限可能(96)
  • 【大模型系列篇】DeepSeek-R1如何通过强化学习有效提升大型语言模型的推理能力?
  • Spring Boot 日志管理(官网文档解读)
  • VSCode自定义快捷键和添加自定义快捷键按键到状态栏
  • 低功耗设计:(3)架构级优化
  • 被裁20240927 --- WSL-Ubuntu20.04安装cuda、cuDNN、tensorRT
  • RoCEv2 高性能传输协议与 Lossless 无损网络