指针:
指针在本质上也是一个变量;指针需要占用一定的内存空间;指针用于保存内存地址的值;
用法格式:[数据类型]* [指针名称]
示例:int* p; char* p;
指针变量的内容是指向内存空间的一段地址,所以指针变量的大小与定义指针的数据类型无关,而指针的大小与系统的地址总线宽度有关,和cpu一次性读取的数据大小有关。常见情况下:
32位系统:在32位操作系统和处理器上,指针通常是32位(4字节)的;
64位系统:在64位操作系统和处理器上,指针通常是64位(8字节)的。
细节:“ void* ”类型的指针也是4个字节的。void*
指针特别之处在于它是一种通用指针类型,可以指向任何类型的数据,但是不能直接进行解引用操作,因为它没有具体的数据类型。
* 号的意义:
1、在指针的声明时,*号表示所声明的变量为指针;
2、在指针使用时,*号表示取指向的内存空间中的值;(解引用)
如何通过指针读写一块内存空间:
1、 要通过指针读取内存中的值,首先需要有一个指向目标内存地址的有效指针,然后使用解引用操作符(*)来获取该地址上存储的值。示例:
#include <stdio.h>
int main() {
int x = 10; // 定义一个整型变量
int *p = &x; // 定义一个指针,初始化为x的地址
// 通过指针读取x的值
int value = *p;
printf("Value at pointer p is: %d\n", value);
return 0;
}
2、向内存写入值也是通过指针完成的。首先确保代码中有一个指向正确内存地址的指针,然后通过解引用操作符(*)来修改该地址上的值。
#include <stdio.h>
int main() {
int x = 10; // 定义一个整型变量
int *p = &x; // 定义一个指针,初始化为x的地址
// 通过指针修改x的值
*p = 20;
printf("New value at pointer p is: %d\n", x);
return 0;
}
既然指针的大小都是4字节或者8字节,那么定义指针的语法中的数据类型关键字有什么作用:
1、类型安全:数据类型指明了湿疹所指向的数据的类型。这帮助了编译器进行类型检查,确保在进行计算中不会讲一个类型的地址值赋给另外一个类型的指针,造成数据丢失。
2、解引用:解引用指针时,数据类型决定了从指针指向的内存地址读取或写入数据是要处理的字节数。例如,int*
指针解引用时会处理4个字节,而 char*
指针解引用时只处理一个字节。
3、指针运算:数据类型关键字对指针进行算术运算(如加法和减法)时非常重要。指针的算术运算依赖于它指向的数据类型的大小。例如,对于 int* p
,表达式 p + 1
会增加 p
的值以指向下一个整数(通常增加4或8字节,取决于int
的大小)。如果指针是 char*
类型,p + 1
则只增加1字节,因为 char
的大小是1字节。
4、函数的参数和返回类型:指针在函数参数或返回类型中用来指示特定类型的数据。
示例:
#include <stdio.h>
int main() {
int num = 1025;
int *p = #
char *cp = (char*)#
printf("Value at p: %d\n", *p); // 输出 1025
printf("Value at cp: %d\n", *cp); // 输出 1 (因为1025的低位字节为1)
return 0;
}
函数的传值调用与传址调用:
传值调用:在传值调用中,当函数被调用时,实际参数的值被复制到函数的形式参数中。在函数内部,形式参数是实际参数的一个局部副本,对形式参数的任何修改都不会影响原始的实际参数。这意味着原始数据是安全的,不会被函数内部的操作改变。
示例:
#include <stdio.h>
void addTen(int x) {
x = x + 10;
printf("Inside addTen: %d\n", x);
}
int main() {
int a = 5;
addTen(a);
printf("In main: %d\n", a); // 输出5,未被addTen函数影响
return 0;
}
传址调用: 在传址调用中,不是将实际参数的值传递给函数,而是传递参数的地址(即指针)。这样,函数接收的是实际参数所在的内存位置的引用。通过这个地址,函数可以直接修改实际参数的值。这种方式允许函数的外部数据通过函数内部的操作被修改。
示例:
#include <stdio.h>
void addTen(int *x) {
*x = *x + 10;
printf("Inside addTen: %d\n", *x);
}
int main() {
int a = 5;
addTen(&a);
printf("In main: %d\n", a); // 输出15,因为addTen修改了a的值
return 0;
}
总结:指针是变量,因此可以声明指针参数,将指针作为函数的参数传递;当一个函数体内部需要改变实参的值,则需要使用指针参数;函数调用时,实参的值复制到形参;指针适用于复杂数据类型作为参数函数中。
数组:
数组是相同类型的变量的有序集合。
数组在一片连续的内存空间中存储元素;数组元素的个数可以显式或隐式指定;数组中的每个元素都可以通过索引(或下标)快速访问,索引通常从0开始。
数组的特点:
1、静态大小:数组的大小在声明时必须指定,并且在其生命周期内不能改变;
2、连续内存位置:数组的所有元素在内存中占据连续的位置,这有助于快速访问任何元素;
3、同一类型元素:数组中的所有元素必须是同一数据类型,例如整数(int
)、字符(char
)或浮点数(float
)。
声明数组:在C语言中,数组的声明需要指定元素的类型和数组的长度
语法格式:【数据类型】 【数组名称】[n];//n表示数组的长度大小
示例:声明一个整形数组,包含10个整数。
int numbers[10];
初始化数组:在声明数组时,还可以初始化数组中的元素;
示例:
int numbers[5] = {10, 20, 30, 40, 50}; // 声明的同时初始化数组
如果在初始化数组时没有提供足够的元素,未被初始化的元素将自动初始化为0:
int numbers[5] = {10, 20}; // numbers将变为{10, 20, 0, 0, 0}
tips:如果想初始化一个数组全为0;那么可以使用便捷的方法:int arr[10] = {0} ;即可
数组名与数组地址之间的关系:
- 数组名代表数组首元素的地址,但是不代表数组的地址;
- 数组的地址需要用取地址符&才能得到;
- 数组名可以看做一个(常量指针)指针常量,指针的指向不可以改变,但是指向地址对应的数据值可以更改;
- 在表达式中,数组只能作为右值使用;
详解:数组名是一个标识符,用于表示数组的起始地址。在大多数表达式中,数组名被解释为一个指针,指向数组的第一个元素。这意味着,如果你有一个数组 int arr[5];
,arr
在使用时几乎总是转换为指向 arr[0]
的指针。然而,数组名本身并不是一个可以重新指向其他位置的变量。它是固定的,总是代表其整个数组的位置。
示例:
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5}; // 数组声明和初始化
int* const ptr = arr; // 指针常量,初始化指向arr的第一个元素
// 通过ptr修改arr的第一个元素
*ptr = 10;
printf("arr[0] = %d\n", arr[0]); // 输出10,显示修改成功
// ptr = arr + 1; // 这是非法的,因为ptr是一个指针常量
return 0;
}
指针与数组之间的关系:
数组名在大多数表达式中用作指向其第一个元素的指针;
可以使用指针运算来遍历数组,类似于使用数组索引;
数组名是一个常量,表示固定的内存地址,不能被重新赋值,而指针是一个变量,可以指向任何匹配类型的地址;
使用sizeof
运算符时,数组名给出的是整个数组的大小,而指针给出的是指针本身的大小;
当数组作为函数参数:
当数组作为参数传递给函数时,它被自动转换为指向其第一个元素的指针。因此,传递给函数的是数组的地址,而非其全部内容;
在函数内部,没有办法直接获取原始数组的大小。
示例:
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
数组与指针之间存在相似性,但是二者仍然不等效使用;
示例:
#include <stdio.h>
int main() {
int numbers[5] = {1, 2, 3, 4, 5};
int *p = numbers; // 直接赋值
printf("Array size: %zu\n", sizeof(numbers)); // 输出数组总大小
printf("Pointer size: %zu\n", sizeof(p)); // 输出指针大小
for (int i = 0; i < 5; i++) {
printf("%d ", *(p + i)); // 使用指针访问数组元素
}
return 0;
}
【难点】以指针的形式访问和以下标的形式访问指针或数组:
在C语言中,以指针形式访问和以下标形式访问指针是两种基本的数据访问方式,它们通常用于处理数组或连续内存区块。这两种方式在表面上看起来有所不同,但实质上是等价的,因为数组访问本质上是通过指针运算实现的。
以指针的形式访问:
以指针的形式访问数据是通过指针加上偏移量来直接访问内存地址的值。这种访问方式依赖于指针运算,其中指针被增加一个偏移量,该偏移量根据指针的类型自动调整(即类型的大小)。这种方法使用解引用操作符(*
)来获取指向的数据。
示例:ptr + 2
计算出一个新的地址,即指针 ptr
的当前位置加上 2
个 int
类型的大小(因为 ptr
是 int
类型的指针)
int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr; // ptr 指向 arr 的第一个元素
// 通过指针加偏移访问第三个元素
int thirdElement = *(ptr + 2); // 等同于 arr[2]
printf("Third element is: %d\n", thirdElement);
以下标的形式访问:
以下标形式访问指针本质上是对指针运算的简化表达。在C语言中,表达式 ptr[i]
实际上是 *(ptr + i)
的简写,这意味着从指针 ptr
当前指向的地址开始,向前移动 i
个由指针类型定义的单位(即跳过 i
个存储单元),然后解引用得到该位置的值。
示例:ptr[2]
直接访问通过 ptr
偏移两个 int
存储单元后的值,它和 ptr + 2
计算出的地址相同,但使用了更直观的数组索引形式
int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr; // ptr 指向 arr 的第一个元素
// 以下标方式访问第三个元素
int thirdElement = ptr[2]; // 等同于 *(ptr + 2) 也等同于 arr[2]
printf("Third element is: %d\n", thirdElement);
总结:数组名本质上也是一个指针,在代码编写中可以视为指针对待,所以既可以通过指针的形式来访问数组,也可以通过下标的形式来访问数组。
数组名a和&a的区别:
1、当使用数组名 a
时,它会被解析(或“退化”)为一个指向数组首元素的指针,这意味着 a
在大多数情况下等同于指向数组第一个元素的指针;当使用 &a
时,它表示取数组 a
的地址。这是一个指向整个数组的指针,而不只是指向第一个元素的指针。
2、如果数组 a
的类型是 T[]
(例如 int[]
),那么 a
的类型在表达式中会被视为 T*
(例如 int*
);如果数组 a
的类型是 T[]
(例如 int[5]
),那么 &a
的类型将是指向数组的指针,即 T (*)[]
(例如 int (*)[5]
)。这表示指向具有特定元素数量的数组的指针。
代码示例:
#include <stdio.h>
int main() {
int a[5] = {1, 2, 3, 4, 5};
// 打印 a 的地址,它会被视为指向数组第一个元素的指针
printf("a 的地址是 %p\n", (void*)a);
// 打印 &a 的地址,它指向整个数组
printf("&a 的地址是 %p\n", (void*)&a);
// 演示不同类型指针的使用差异
int *p1 = a; // 正确:p1 指向数组 a 的第一个元素
int (*p2)[5] = &a; // 正确:p2 指向整个数组 a
// 通过 p1 访问第一个元素
printf("通过 p1 访问第一个元素: %d\n", *p1);
// 通过 p2 访问第一个元素
printf("通过 p2 访问第一个元素: %d\n", (*p2)[0]);
return 0;
}
数组作为函数参数:
当数组作为函数的参数传入时,C语言中的编译器处理方式有几个特点:
1、数组退化为指针
在C语言中,数组名在大多数表达式中退化为(转换为)指向其第一个元素的指针,例如,如果你有一个数组 int arr[10];
并将其作为参数传递给一个函数,这个数组在传递给函数时实际上传递的是一个指向 int
的指针,指向数组的第一个元素;
2、函数形参的声明
函数在接收数组作为参数时,通常形参会被声明为指针类型,因为数组已经退化为指针;
3、数组的大小信息丢失
因为数组退化成了指针,所以当数组作为参数传递给函数时,原始数组的大小信息不会被传递;
4、对原始数组的修改
由于传递给函数的是原始数组的地址,函数内对这个指针指向的数据的任何修改都会影响到原始数组。
指针与数组之间区别详解:
指针是一种特殊的变量,与整数的运算规则是:
p + n ;等价于(unsigned int)p + n*sizeof(*p);
结论:当指针p指向同一个类型的数组的元素时:p+1将指向当前元素的下一个元素;p-1将指向当前元素的上一个元素;
指针的运算:
指针之间只支持减法运算,且必须参与运算的指针类型必须相同。即:指针的减法通常用于计算两个指向同一数组元素的指针之间的元素数量差异。
假设有两个指针 ptr1
和 ptr2
,它们都指向同一类型的数据,并且指向同一个数组的不同元素。当你计算 ptr1 - ptr2
时,C语言中的计算公式是:
示例:在这个示例中,ptr2 - ptr1
会输出 3,表示从 ptr1
指向的元素到 ptr2
指向的元素之间有三个整型元素的距离。
#include <stdio.h>
int main() {
int array[5] = {10, 20, 30, 40, 50};
int *ptr1 = &array[1]; // 指向 array[1]
int *ptr2 = &array[4]; // 指向 array[4]
printf("Elements between ptr1 and ptr2: %ld\n", ptr2 - ptr1);
return 0;
}
指针的比较:
在c语言中,指针之间可以进行关系运算。关系运算允许在代码中比较两个指针的地址值,以确定他们在内存中的相对位置。关系包括:
==
(等于)!=
(不等于)<
(小于)>
(大于)<=
(小于等于)>=
(大于等于)
运算条件:进行关系运算的指针通常应该是同类型的。
使用场景:关系运算一般出现在数组遍历,排序算法中的指针操作。
示例:这个例子利用了字符数组的性质(以 \0
为结束标志),以及指针关系运算来有效地遍历和处理字符串。
#include <stdio.h>
int main() {
char text[] = "Hello, C!";
char *ptr = text; // 指向字符数组(字符串)的开始
// 使用指针遍历字符串,直到遇到终止字符 '\0'
while (*ptr != '\0') {
printf("%c\n", *ptr); // 输出当前字符
ptr++; // 移动指针到下一个字符
}
return 0;
}
字符串:
其实C语言中并没有字符串数据类型;在c语言中使用字符数组来模拟字符串;C语言中的字符串是以'\0'结束的字符组;C语言中的字符串可以分配于栈空间、堆空间或者只读存储区;
如何确定字符串的存储位置:
1、只读存储区:通常是直接将字符串字面量赋值给指针;例如:
char *s = "hello";
2、栈空间:在函数内部声明的字符数组,使用字符串字面量初始化或者手动赋值字符;例如:
char s[] = "hello";
3、堆空间:通过 malloc
, calloc
等动态内存分配函数分配的内存,并用来存储字符串;例如:
char *s = malloc(6); // 分配足够的空间来存储 "hello" 和空字符
if (s != NULL) {
strcpy(s, "hello");
}
字符串的长度:
字符串长度是字符串所包含的字符个数;但是C语言中的字符串长度指的是第一个'\0'字符前出现的字符个数;通常通过'\0'这个结束字符来判断字符串的长度。
不受限制的字符串及函数:
"不受限制的字符串函数"通常指的是那些在处理字符串时不自动检查目标缓冲区大小的函数。使用这些函数可能会导致缓冲区溢出,因为它们不会阻止复制或连接超过目标数组容量的数据。
不受限制的字符串函数是通过寻找字符串的结束符'\0'来判断长度;
详细介绍下列三个字符串函数:
字符串复制函数、字符串链接函数、字符串比较函数。
char *strcpy(char *dest, const char *src);
char *strcat(char *dest, const char *src);
int strcmp(const char *s1, const char *s2);
strcpy
(字符串复制):dest
: 目标字符串数组,应该有足够的空间来容纳源字符串;src
: 源字符串,一个以空字符 \0
结尾的字符数组;
示例:
#include <stdio.h>
#include <string.h>
int main() {
char src[] = "Hello, world!";
char dest[50]; // 确保有足够空间
strcpy(dest, src);
printf("Copied string: %s\n", dest);
return 0;
}
strcat
(字符串连接):
dest
: 目标字符串数组,必须以空字符 \0
结尾,并应有足够的空间来容纳追加的源字符串;
src
: 要追加到 dest
后面的源字符串;
示例:
#include <stdio.h>
#include <string.h>
int main() {
char dest[20] = "Hello";
char src[] = ", world!";
strcat(dest, src); // 确保dest有足够空间
printf("Concatenated string: %s\n", dest);
return 0;
}
strcmp
(字符串比较):
s1
, s2
: 要比较的两个字符串;
返回值:如果 s1
和 s2
字符串相等,返回0。如果 s1
字典序小于 s2
,返回负数。如果 s1
字典序大于 s2
,返回正数;
示例:
#include <stdio.h>
#include <string.h>
int main() {
char str1[] = "hello";
char str2[] = "world";
int result = strcmp(str1, str2);
if (result == 0) {
printf("The strings are identical.\n");
} else if (result < 0) {
printf("String 1 is less than String 2.\n");
} else {
printf("String 1 is greater than String 2.\n");
}
return 0;
}
面试重要考题:编程实现上述三个函数的功能。
受限制的字符串函数:
相比上文中的不受限制字符串函数,即在操作过程中不比考虑需操作的字符串的长度,但是在受限制的字符串函数中,在处理字符串时会考虑目标缓冲区大小。这些函数会在进行字符串操作时,检查目标缓冲区的大小,以确保不会发生缓冲区溢出的情况。
-
strncpy
:受限制的字符串复制函数,用于将源字符串的一部分复制到目标字符串中,可以指定要复制的最大字符数。char *strncpy(char *dest, const char *src, size_t n);
dest
:目标字符串数组的指针,用于存储复制后的字符串
src
:源字符串的指针,要复制的字符串
n
:最大复制的字符数,包括终止的空字符 -
strncat
:受限制的字符串连接函数,用于将源字符串的一部分连接到目标字符串的末尾,可以指定要连接的最大字符数。char *strncat(char *dest, const char *src, size_t n);
dest
:目标字符串数组的指针,要连接的字符串的目标位置src
:源字符串的指针,要连接的字符串n
:最大连接的字符数,包括终止的空字符 -
strncmp
:受限制的字符串比较函数,用于比较两个字符串的一部分,可以指定要比较的最大字符数。int strncmp(const char *s1, const char *s2, size_t n);
s1
:要比较的第一个字符串的指针s2
:要比较的第二个字符串的指针n
:要比较的最大字符数 -
strnlen
:受限制的字符串长度函数,用于计算字符串的长度,但最多计算指定的最大字符数,避免在未知长度的字符串中遇到未定义的行为。size_t strnlen(const char *s, size_t maxlen);
s
:要计算长度的字符串的指针maxlen
:最大要计算的字符数