前言
今天我们继续说指针,上一篇讲述了指针的基础,这一篇着重讲述指针的进阶
即:指针 + 一维整型数组 ; 指针 + 一维字符型数组。 开始啦~ ^ - ^
一、指针 + 一维整型数组
1、回顾一维整型数组
以 int a[5] 为例:
数组名:a //a的值是数组首元素的地址,是一个常量,不能做自增自减运算
类型:int[5]; // 数据类型
2、数组名做指针
定义一个整型数组 a[5]={1,2,3,4,5};
int *p = a; // a 是 数组名 ,a 的值是数组首元素的地址;这个式子表示指向数组a。
int *p = &a[0]
总结:在C语言中,数组名本身就是一个指向数组第一个元素的指针常量。
例如在 int a[5] 中,a 表示一个指向 a[0] 的指针,它的类型是 int *。
3、指针与数组元素访问
可以使用指针来遍历和访问数组的元素。
例如 数组a[ i ], 可以使用 *(a+1)来替代,其中 a 是数组的首地址。 // a+1代表偏移到下一个地址
4、指数算数运算
指数可以进行算数运算,通过增加或者减少指针的值来访问数组中的不同元素。
例如 arr + i 指向数组中第 i 个元素的地址, * (arr + i) 获取该元素的值。
运算符: & *
p + N //指向
p
指向的类型的第N
个元素的地址。p - N //指向
p
指向的类型的第N
个元素前面的地址。p + +
p - -
指针比较 > >= < <= == !=
p - q //两个指针相减,表示差了多少个元素
//两个指针相减的结果是一个整数,表示这两个指针之间的偏移量(以元素为 单 位)。这种偏移量通常用来计算数组中元素之间的距离或者指针在内存中的 相对位置。要注意的一点是:两个指针必须指向同一类型的数据
p + q 不存在,没有意义
*&p==p, //* & 是逆运算,按照结合的优先性,先计算&p(取p的地址),在计算*(&p),取 出该地址的值 还是 p,即
p
的值是指针p
本身存储的地址。*(p+i) <=> a[i] <=> *(a+i)
a[i] <=> *(a+i)
i[a] <=> *(i+a)
5、数组作为函数参数
当数组作为函数参数传递时,实际上传递的是数组的首地址。这允许函数能够直接访问和修改数组的元素。
形参 ------- 数组形式 要写成 int *a // 实际上是指针类型变量
数组长度 int len
实参 ------- 数组名 //代表数组首元素的地址
数组长度
演示代码
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr; // ptr 指向数组 arr 的首地址
// 使用指针遍历和访问数组元素
printf("Array elements via pointer:\n");
for (int i = 0; i < 5; ++i) {
printf("%d ", *(ptr + i)); // 或者可以写成 ptr[i]
}
printf("\n");
// 修改数组元素的值
*(ptr + 2) = 35; // 修改第三个元素的值为 35
// 打印修改后的数组
printf("Array after modification:\n");
for (int i = 0; i < 5; ++i) {
printf("%d ", arr[i]);
}
printf("\n");
return 0;
}
二、指针 + 一维字符型数组
1、字符串与字符数组
一维字符型数组通常用来存储字符串。
字符串在C语言中 被 定义为 以空字符 '\0' 结尾的 字符数组。
例如
char str[] = "Hello"; // 字符数组存储字符串
这里的 str
是一个字符数组,它包含了字符 'H', 'e', 'l', 'l', 'o', '\0'
。
2、指针与字符数组
指针在C语言中常用来处理字符串。一个指向字符的指针可以指向字符串的第一个字符,也可以遍历整个字符串或者操作字符串中的特定元素。
char str[] = "Hello";
char *ptr = str; // 指向字符数组的指针
printf("%c\n", *ptr); // 打印第一个字符 'H'
// 遍历字符串
while (*ptr != '\0') {
printf("%c", *ptr);
ptr++;
}
三、字符串操作函数与指针
1、const 关键字
当涉及到指针和字符串时,const
关键字可以在多种情况下发挥重要作用,特别是在指针和字符串操作中。以下是 const
在这些上下文中的主要作用和知识点总结:
1、const
修饰指针
就近原则,离谁进就限定谁。
const
修饰指针本身:
(1)
const
可以放在 *
前面,表示指针指向的数据是常量,不能通过该指针修改数据。
const int *p = &a;
int const *p = &a;
这表示 p
是一个指向 int
类型常量的指针,不能通过 p
修改指向的 int
数据。
(2)
const
也可以放在 *
后面,表示指针本身是常量,即指针变量不能被修改指向其他地址。
int * const ptr;
这表示 ptr
是一个常量指针,其指向不能改变,但可以通过 ptr
修改指向的数据。
(3)const
可以同时放在 *
前后,表示指针本身和指针指向的数据都是常量。
const int * const ptr;
这表示 ptr
是一个指向 const int
类型常量的常量指针,既不能通过 ptr
修改指向的数据,也不能修改 ptr
指向的地址。
2、const
修饰字符串
(1)const
修饰字符数组(字符串常量):
在声明字符串常量时,可以使用 const
保证该字符串在程序执行过程中不会被修改。
const char str[] = "Hello";
这里 str
是一个常量字符数组,不能通过 str
修改数组中的字符。
(2)const
修饰指向字符串的指针:
使用 const
修饰指向字符串的指针可以避免通过该指针修改字符串的内容。
const char *ptr = "World";
这表示 ptr
是一个指向字符常量的指针,不能通过 ptr
修改指向的字符串内容。
(3) const
修饰指向指针的指针:
在处理指向字符串的指针时,可以使用 const
修饰指向指针的指针,确保不会通过该指针修改指向字符串的指针本身。
char * const *ptr;
这表示 ptr
是一个常量指针的指针,即 ptr
本身不能修改为指向其他地址,但可以通过 ptr
修改指向的字符串指针。
3、使用const的好处
-
安全性:通过将
const
应用于指针和字符串,可以避免意外的数据修改,提高程序的安全性和可靠性。 -
约束性:使得代码更加清晰明了,告诉编译器不允许某些操作,有助于减少程序出错的可能性。
-
优化:在某些情况下,编译器可以通过
const
优化代码,例如将常量数据放在只读段(read-only segment)中。
4、形参、实参
- 形参 :设计为 const char *,
- 目的: 防止函数中的误操作
- 好处 :
(1) 提前发现问题,将运行时问题,提前到编译时。
(2) const char *,可以接收 char *,const char *,提高了代码的健壮性。
2.实参:
- 可以是数组名
- 可以是值针变量 char *p //const char *p
- 可以是直接是一个字符串常量 ,例如“hello”
- 提高参数的适用性。
注意:
能写成const的 都写const
2、puts 函数
语法:
int puts (const char *s);
代码实现
int Puts(const char *s)
{
if (s == NULL)
{
return -1;
}
while (*s != '\0')
{
putchar(*s++);
}
putchar('\n');
return 0;
}
int main(void)
{
char s[] = "hello";
char *p = s; //const char *p = "hello"; //const char *p = s;
Puts("hello"); //Puts(s)
return 0;
}
3、gets 函数
语法:
char * Gets(char *s)
代码实现:
char * Gets(char *s)
{
char *ret = s;
while ((*s = getchar())!='\n')
{
++s;
}
*s = '\0';
return ret;
}
返回值,成功的时候,就是 存字符串的 空间的首地址
4、strlen 函数
语法:
size_t Strlen(const char *s)
代码实现
size_t Strlen(const char *s)
{
const char *ret = s;
while (*s!='\0')
++s;
return s-ret;
}
5、strcpy 函数 与 strncpy 函数
(1)strcpy 函数
语法:
char * Strcpy(char *dest,const char *src)
代码实现:
char * Strcpy(char *dest,const char *src)
{
char *ret = dest;
while (*src != '\0')
{
*dest = *src;
++dest;
++src;
}
*dest = '\0';
return ret;
}
(2)strncpy 函数
语法:
char * Strncpy(char *dest,const char *src,size_t n)
思路:
- 始终拷贝了n下
1. 拷贝
遇到‘\0’或者n=0时,拷贝的循环结束
2. 考虑n
n有没有结束
如果n>src字符串的长度,完成剩余的次数的拷贝,给dest字符串赋值‘\0’;
如果n<src字符串的长度,n等于多少,就拷贝多少个元素。
代码实现:
char * Strncpy(char *dest,const char *src,size_t n)
{
char *ret = dest;
while ( n!=0 &&*src != '\0')
{
*dest = *src;
++dest;
++src;
--n;
}
while(n)
{
*dest = '\0';
++dest;
--n;
}
return ret;
}
返回值:
//指针类型
成功 返回目标字符串所在的地址
补充 size_t
size_t
是一个在 C 和 C++ 等编程语言中经常使用的数据类型,它代表着内存中对象的大小(即尺寸),通常用于表示数组的索引或是某些内存操作的返回值类型。具体来说:
-
size_t
是一个无符号整数类型。 -
它的大小是根据系统架构来决定的,通常是足以存储内存中任意对象的大小。
-
在大多数情况下,
size_t
的大小和unsigned int
或unsigned long
相同,但不同的编译器和操作系统可能有所不同。 -
size_t
是标准库<stdio.h>
中定义的,因此在使用时通常需要包含这个头文件。
主要用途:
-
数组索引: 在访问数组元素或进行循环迭代时,使用
size_t
类型作为索引类型,因为它足够大以表示数组的大小。 -
内存分配函数的返回值类型: 例如
malloc
、calloc
、realloc
等函数返回的是void*
,而用于表示内存大小的参数通常使用size_t
类型。 -
字符串长度和大小: 用于表示字符串的长度或者某些字符串处理函数的返回值类型,比如
strlen
函数返回的是size_t
类型的字符串长度。
6、 strcat 函数 与 strncat 函数
(1)strcat 函数
语法:
char * Strcat(char *dest,const char *src)
思路:
//1.定位 dest的 '\0'
//2.拷贝
//3.给dest字符串赋值'\0'
代码实现:
char * Strcat(char *dest,const char *src)
{
char *ret = dest;
while (*dest != '\0')
++dest;
while (*src != '\0')
{
*dest = *src;
++dest;
++src;
}
*dest = '\0';
return ret;
}
(2)strncat 函数
语法:
char * Strncat(char *dest,const char *src,int n)
思路 :
//1.可以指定 n
//如果 src 长度 > n
就将前n个字符拼接过去
//如果 src 长度 < n
直接将src字符串拼接过去
最终 一定要保证 dest是一个字符串 '\0'
代码实现:
char * Strncat(char *dest,const char *src,int n)
{
char *ret = dest;
while (*dest != '\0')
++dest;
while ( n && *src != '\0')
{
*dest = *src;
++dest;
++src;
--n;
}
*dest = '\0';
return ret;
}
返回值:
//指针类型
成功 返回目标字符串所在的地址
7、strcmp 函数与 strncmp 函数
(1)strcmp 函数
语法:
int Strcmp(const char *s1,const char *s2)
代码实现:
int Strcmp(const char *s1,const char *s2)
{
while (*s1==*s2 && *s1!='\0' && *s2!='\0')
{
++s1;
++s2;
}
return *s1 - *s2;
}
(2)strncmp 函数
语法:
int Strncmp(const char *s1,const char *s2,size_t n)
代码实现:
int Strncmp(const char *s1,const char *s2,size_t n)
{
//n = 3 s1 s2
//n = 2 s1+1 s2+1
//n = 1 s1+2 s2+2
// s1+3 s2+3 //n = 0
while ( n > 1&&*s1==*s2 && *s1!='\0' && *s2!='\0')
{
++s1;
++s2;
--n;
}
return *s1 - *s2;
}
注意:
strcpy、strcat、strcmp,这三个函数对应的strncpy函数、 strncat函数、 strncmp函数
n 都是指,从原字符串复制(剪切)n个元素到目标字符串中
在strncmp中,n 是指让两个字符串中的前n个元素相比。
四、链式操作
strcpy函数、strcmp函数的返回值都是char*
即指针类型,返回到了目标字符串所在的地址。
这样做,是为了方便进行 链式操作
下面介绍一下链式操作:
链式操作(Chaining)是一种编程风格或技术,允许在一行代码中依次调用多个方法或函数,并且每个方法都在前一个方法的返回结果上操作,形成一个连续的操作链。这种方式可以使代码更加简洁和易读,特别是当需要对同一个对象进行一系列的操作时特别有用。
特点和用法:
-
连续调用:链式操作允许在同一个表达式中依次调用多个方法或函数,每个方法都基于上一个方法的返回值进行操作。
-
返回值的要求:每个方法或函数必须返回一个对象或值,以便后续方法可以继续调用。
-
语法:在支持链式操作的语言或框架中,通常方法调用会直接跟在上一个方法调用的后面,使用
.
或者其他合适的语法连接。
例如:
char s1[10];
char s2[10];
strcpy(s2,strcpy(s1,"hello"));
返回值类型是指针类型在 C 语言中对于支持链式操作提供了直接的数据访问和修改能力,使得代码可以更加模块化和灵活:
-
修改原始数据:通过返回指向结构体或者动态分配内存的指针,可以在函数调用链中直接修改原始数据。这种方式避免了在每个函数调用中都复制大量数据,提高了程序的效率和性能。
-
连续调用:返回指针类型允许函数调用链的连续性,即在一个函数返回的指针上直接调用另一个函数。这种方式类似于链式调用的风格,使得代码更加紧凑和易于理解。
-
动态分配内存:如果结构体较大或需要在堆上动态分配内存,返回指针类型可以避免复制整个结构体的开销,同时确保函数调用链中每个步骤都可以有效地访问和修改相同的数据。
-
状态维护:指针类型返回值使得函数可以直接操作和修改数据结构的状态,而无需依赖全局变量或者复杂的参数传递。这种方式尤其在需要处理复杂数据结构或者对象的情况下非常有用。
尽管指针类型的返回值提供了上述优势,但同时也需要小心管理内存和避免悬挂指针问题。在使用指针类型返回值时,确保返回的指针指向有效的内存,且在适当的时候进行内存释放,以避免内存泄漏和未定义行为。
五、memcpy 函数(包含void*用法)
(1)memcpy函数语法:
void *memcpy(void *dest, const void *src, size_t n)
{
//一个字节一个字节拷贝
}
(2)void *
//NULL 空指针
//空类型的指针 --- 万能指针
//可以接收任意类型的指针
总结
指针 + 一维数组,指针 + 一维字符型数组 是后续学习 指针 + 二维数组,指针 + 二位字符型数组的基础,一定要理解指针的含义,它是用来接受另一个变量的地址的。指针的运算(定位内存中的数据地址),方便了取用内存中的数据。同时也要牢固掌握如何定义指针的类型,以及在操作字符串的函数中,熟练运用指针。