开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 21 天,点击查看活动详情
指针
什么是指针
程序中使用的变量实际上都在内存中创建的,每个变量都会被保存在内存的某一个位置上(具体在哪个位置是由系统分配的),就像我们最终会在这个世界上的某个角落安家一样,所有的变量在对应的内存位置上都有一个地址(地址是独一无二的),而我们可以通过这个地址寻找到这个变量本体,比如int占据4字节,因此int类型变量的地址就是这4个字节的起始地址,后面32个bit位全部都是用于存放此变量的值的。
这里的0x是十六进制的表示形式(10-15用字母A - F表示)如果我们能够知道变量的内存地址,那么无论身在何处,都可以通过地址找到这个变量了。而指针的作用,就是专门用来保存这个内存地址的。
我们来看看如何创建一个指针变量用于保存变量的内存地址:
#include <stdio.h>
int main(){
int a = 10;
//指针类型需要与变量的类型相同,且后面需要添加一个*符号表示是对于类型的指针
int * p = &a; //这里的&是取地址操作,也就是拿到变量a的地址
printf("a在内存中的地址为:%p", p); //地址使用%p表示
}
可以看到,我们通过取地址操作&,将变量a的地址保存到了一个地址变量p中。
拿到指针之后,我们可以获取指针所指地址上的值:
#include <stdio.h>
int main(){
int a = 666;
int * p = &a;
printf("内存%p上存储的值为:%d", p, *p); //我们可以在指针变量前添加一个*号(间接运算符,也可以叫做解引用运算符)来获取对应地址存储的值
}
同样的,我们也可以直接像这样去修改对应地址存放的值:
#include <stdio.h>
int main(){
int a = 666;
int * p = &a;
*p = 999; //通过*来访问对应地址的值,并通过赋值运算对其进行修改
printf("a的值为:%d", a);
}
实际上拿到一个变量的地址之后,我们完全不需要再使用这个变量,而是可以通过它的指针来对其进行各种修改。因此,现在我们想要实现对两个变量的值进行交换的函数就很简单了:
#include <stdio.h>
// 这里是两个指针类型的形参,其值为实参传入的地址,
void swap(int *a, int *b){
int tmp = *a; //先暂存一下变量a地址上的值
*a = *b; //将变量b地址上的值赋值给变量a对应的位置
*b = tmp; //最后将a的值赋值给b对应位置,这样就成功交换两个变量的值了
}
int main(){
int a = 10, b = 20;
swap(&a, &b); //只需要把a和b的内存地址给过去就行了,这里取一下地址
printf("a = %d, b = %d", a, b);
}
当然,和变量一样,要是不给指针变量赋初始值的话,就不知道指的哪里了,因为指针变量也是变量,存放的其他变量的地址值也在内存中保存,如果不给初始值,那么存放别人地址的这块内存可能在其他地方使用过,这样就不知道初始值是多少了,所以一定要记得给个初始值或是将其设定为NULL,表示空指针,不指向任何内容。
#include <stdio.h>
int main(){
int * a = NULL;
}
我们接着来看看const类型的指针,这种指针比较特殊:
#include <stdio.h>
int main(){
int a = 9, b = 10;
const int * p = &a;
printf("%p\n", p); // 0x16b56331c
p = &b; //但是指针指向的地址是可以发生改变的
printf("%p", p); // 0x16b563318
}
我们再来看另一种情况:
#include <stdio.h>
int main(){
int a = 9, b = 10;
int * const p = &a; //const关键字被放在了类型后面
*p = 20; //允许修改所指地址上的值
p = &b; //但是不允许修改指针存储的地址值,其实就是反过来了。
}
指针与数组
数组表示法实际上是在变相地使用指针,可以将其理解为数组变量其实就是一个指针变量,它存放的就是数组中第一个元素的起始地址。
我们尝试一下:
#include <stdio.h>
int main(){
char str[] = "Hello World!";
char * p = str;
printf("%c", *p);
}
int main(){
char str[] = "Hello World!";
char * p = str;
printf("%c", p[1]);
}
数组在内存中是一块连续的空间,所以为什么声明数组一定要明确类型和大小,因为这一块连续的内存空间生成后就固定了。
而我们的数组变量实际上存放的就是首元素的地址,而实际上我们之前一直使用的都是数组表示法来操作数组,这样可以很方便地让我们对内存中的各个元素值进行操作:
int main(){
char str[] = "Hello World!";
printf("%c", str[0]); //直接在中括号中输入对应的下标就能访问对应位置上的元素了
}
而我们知道实际上str表示的就是数组的首地址,所以我们完全可以将其赋值给一个指针变量,因为指针变量也是存放的地址:
char str[] = "Hello World!";
char * p = str; //直接把str代表的首元素地址给到p
而使用指针后,实际上我们可以使用另一种表示法来操作数组,这种表示法叫做指针表示法:
int main(){
char str[] = "Hello World!";
char * p = str;
printf("第一个元素值为:%c,第二个元素值为:%c", *p, *(p+1)); //通过指针也可以表示对应位置上的值
}
比如我们现在需要表示数组中的第二个元素:
- 数组表示法:
str[1] - 指针表示法:
*(p+1)
虽然写法不同,但是他们表示的意义是完全相同的,都代表了数组中的第二个元素,其中指针表示法使用了p+1的形式表示第二个元素,这里的+1操作并不是让地址+1,而是让地址+ 一倍的对应类型大小,也就是说地址后移一个char的长度,所以正好指向了第二个元素,然后通过*取到对应的值
*(p+i) <=> str[i] //实际上就是可以相互转换的
我们来看看下面的各个表达式分别代表什么:
*p //数组的第一个元素
p //数组的第一个元素的地址
p == str //肯定是真,因为都是数组首元素地址
*str //因为str就是首元素的地址,所以这里对地址加*就代表第一个元素,使用的是指针表示法
&str[0] //这里得到的实际上还是首元素的地址
*(p + 1) //代表第二个元素
p + 1 //第二个元素的内存地址
*p + 1 //注意*的优先级比+要高,所以这里代表的是首元素的值+1,得到字符'I'
当然指针也可以进行自增和自减操作,比如:
#include <stdio.h>
int main(){
char str[] = "Hello World!";
char * p = str;
p++; //自增后相当于指针指向了第二个元素的地址
printf("%c", *p); //所以这里打印的就是第二个元素的值了
}
一维数组看完了,我们来看看二维数组,那么二维数组在内存中是如何表示的呢?
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
所以虽然我们可以使用二维数组的语法来访问这些元素,但其实我们也可以使用指针来进行访问:
#include <stdio.h>
int main(){
int arr[][3] = {{1, 2, 3}, {4, 5, 6}};
int * p = arr[0]; //因为是二维数组,注意这里要指向第一个元素,来降一个维度才能正确给到指针
//同理如果这里是arr[1]的话那么就表示指向二维数组中第二个数组的首元素
printf("%d = %d", *(p + 4), arr[1][1]); //实际上这两种访问形式都是一样的
}
多级指针
我们知道,实际上指针本身也是一个变量,它存放的是目标的地址,但是它本身作为一个变量,它也要将地址信息保存到内存中,所以,实际上当我们有指针之后,也需要在内存中开辟一块空间来存放。我们还可以继续创建一个指向指针变量地址的指针,甚至可以创建更多级。
#include <stdio.h>
int main(){
int a = 20;
int * p = &a; //指向普通变量的指针
//因为现在要指向一个int *类型的变量,所以类型为int* 再加一个*
int ** pp = &p; //指向指针的指针(二级指针)
int *** ppp = &pp; //指向指针的指针的指针(三级指针)
printf("a的地址为%p a的值为%d\n", p, *p);
printf("*p的地址为%p *p的值为%d\n", pp, *pp);
printf("**pp的地址为%p **pp的值为%d\n", ppp, *ppp);
}
那么我们如何访问对应地址上的值呢?
#include <stdio.h>
int main(){
int a = 20;
int * p = &a;
int ** pp = &p;
printf("p = %p, a = %d", *pp, **pp); //使用一次*表示二级指针指向的指针变量,继续使用一次*会继续解析成指针变量所指的普通变量
}
本质其实就是一个套娃而已,只要把各个层次分清楚,实际上还是很好理解的。
指针数组与数组指针
前面我们了解了指针的一些基本操作,包括它与数组的一些关系。我们接着来看指针数组和数组指针,这两词语看着就容易搞混,不过哪个词在后面就哪个,我们先来看指针数组,虽然名字很像数组指针,但是它本质上是一个数组,不过这个数组是用于存放指针的数组。
#include <stdio.h>
int main(){
int a, b, c;
int * arr[3] = {&a, &b, &c}; //可以看到,实际上本质还是数组,只不过存的都是地址
}
因为这个数组中全都是指针,比如现在我们想要访问数组中第一个指针指向的地址:
#include <stdio.h>
int main(){
int a, b, c;
int * arr[3] = {&a, &b, &c};
*arr[0] = 999; //[]运算符的优先级更高,所以这里先通过[0]取出地址,然后再使用*将值赋值到对应的地址上
printf("%d", a);
}
当然我们也可以用二级指针变量来得到指针数组的首元素地址:
#include <stdio.h>
int main(){
int * p[3]; //因为数组内全是指针
int ** pp = p; //所以可以直接使用指向指针的指针来指向数组中的第一个指针元素
}
实际上指针数组还是很好理解的,那么数组指针呢?可以看到指针在后,说明本质是一个指针,不过这个指针比较特殊,它是一个指向数组的指针。
比如:
int * p; //指向int类型的指针
而数组指针则表示指向整个数组:
int (*p)[3]; //注意这里需要将*p括起来,因为[]的优先级更高
注意它的目标是整个数组,而不是普通的指针那样指向的是数组的首个元素:
int arr[3] = {111, 222, 333};
int (*p)[3] = &arr; //直接对整个数组再取一次地址
那么现在已经取到了指向整个数组的指针,该怎么去使用呢?
#include <stdio.h>
int main(){
int arr[3] = {111, 222, 333};
int (*p)[3] = &arr; //直接对整个数组再取一次地址
printf("%d, %d, %d", *(*p+0), *(*p+1), *(*p+2)); //要获取数组中的每个元素,稍微有点麻烦
}
注意此时:
p代表整个数组的地址*p表示所指向数组中首元素的地址*p+i表示所指向数组中第i个(0开始)元素的地址(实际上这里的*p就是指向首元素的指针)*(*p + i)就是取对应地址上的值了
虽然在处理一维数组上感觉有点麻烦,但是它同样也可以处理二维数组:
int arr[][3] = {{111, 222, 333}, {444, 555, 666}};
int (*p)[3] = arr; //二维数组不需要再取地址了,因为现在维度提升,数组指针指向的是二维数组中的其中一个元素
比如现在我们想要访问第一个数组的第二个元素,根据上面p各种情况下的意义:
printf("%d", *(*p+1)); //因为上面直接指向的就是第一个数组,所以想要获取第一个元素和之前是一模一样的
那么要是我们现在想要获取第二个数组中的最后一个元素呢?
printf("%d", *(*(p+1)+2); //首先*(p+1)为一个整体,表示第二个数组(因为是数组指针,所以这里+1一次性跳一个数组的长度),然后再到外层+2表示数组中的第三个元素,最后再取地址,就是第二个数组的第三个元素了
当然也可以使用数组表示法:
printf("%d", p[1][2]); //类似二维数组的用法
实战:合并两个有序数组
来源:力扣 No.88 合并两个有序数组:leetcode.cn/problems/me…
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
示例 1:
输入:nums1 = [1,2,3,0,0,0], m = 3, nums2 = [2,5,6], n = 3 输出:[1,2,2,3,5,6] 解释:需要合并 [1,2,3] 和 [2,5,6] 。 合并结果是 [1,2,2,3,5,6] ,其中斜体加粗标注的为 nums1 中的元素。
示例 2:
输入:nums1 = [1], m = 1, nums2 = [], n = 0 输出:[1] 解释:需要合并 [1] 和 [] 。 合并结果是 [1] 。
示例 3:
输入:nums1 = [0], m = 0, nums2 = [1], n = 1 输出:[1] 解释:需要合并的数组是 [] 和 [1] 。 合并结果是 [1] 。 注意,因为 m = 0 ,所以 nums1 中没有元素。nums1 中仅存的 0 仅仅是为了确保合并结果可以顺利存放到 nums1 中。
假设有两个指针指向两个数组的最后(第一个数组是有效数字的最后一个),依次比较大小,较大的元素放到后面。
#include <stdio.h>
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
int i = m - 1, j = n - 1;
for (int k = m + n - 1; k >= 0; --k) {
if (i < 0) {
nums1[k] = nums2[j--];
} else if (j < 0) {
nums1[k] = nums1[i--];
} else {
if (nums1[i] < nums2[j]) {
nums1[k] = nums2[j--];
} else {
nums1[k] = nums1[i--];
}
}
}
}
#include <stdio.h>
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n){
int i = m - 1, j = n - 1;
for (int k = m + n - 1; k >= 0; --k) {
if (i < 0) {
*(nums1 + k) = *(nums2 + j--);
} else if (j < 0) {
*(nums1 + k) = *(nums1 + i--);
} else {
if (*(nums1 + i) > *(nums2 + j)) {
*(nums1 + k) = *(nums1 + i--);
} else {
*(nums1 + k) = *(nums2 + j--);
}
}
}
}
实战:二维数组中的查找
来源:剑指Offer 04. 二维数组中的查找:leetcode.cn/problems/er…
在一个 n * m 的二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。请完成一个高效的函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。
示例:
现有矩阵 matrix 如下:
[ [1, 4, 7, 11, 15], [2, 5, 8, 12, 19], [3, 6, 9, 16, 22], [10, 13, 14, 17, 24], [18, 21, 23, 26, 30] ]
给定 target = 5,返回 true。
给定 target = 20,返回 false。
#include <stdio.h>
#include <stdbool.h>
/**
*
* @param matrix 二维数组
* @param matrixSize 高度
* @param matrixColSize 宽度
* @param target 目标数
* @return
*/
bool findNumberIn2DArray(int** matrix, int matrixSize, int* matrixColSize, int target){
if(matrixSize == 0 || * matrixColSize == 0) return false;
int x = *matrixColSize - 1, y = 0;
while (x >= 0 && y < matrixSize) {
if(matrix[y][x] > target) x--;
else if (matrix[y][x] < target) y++;
else return true;
}
return false;
}
#include <stdio.h>
#include <stdbool.h>
/**
*
* @param matrix 二维数组
* @param matrixSize 高度
* @param matrixColSize 宽度
* @param target 目标数
* @return
*/
bool findNumberIn2DArray(int **matrix, int matrixSize, int *matrixColSize, int target) {
if (matrixSize == 0 || *matrixColSize == 0) return false;
int x = *matrixColSize - 1, y = 0;
while (x >= 0 && y < matrixSize) {
if (*(*(matrix + y) + x) > target) x--;
else if (matrix[y][x] < target) y++;
else return true;
}
return false;
}
结构体、联合体和枚举
创建和使用结构体
结构体类似于java中的实体类,自定义名称和内部属性,下面我们来看如何创建结构体:
struct Student { //使用 (struct关键字 + 结构体类型名称) 来声明结构体类型,这种类型是我们自己创建的
int id; //结构体中可以包含多个不同类型的数据,这些数据共同组成了整个结构体类型(当然结构体内部也能包含结构体类型的变量,类似java)
int age;
char * name; //用户名可以用指针指向一个字符串,也可以用char数组来存
};
int main() {
struct Student { //也可以以局部形式存在
};
}
定义好结构体后,我们只需要使用结构体名称作为类型就可以创建一个结构体变量了:
#include <stdio.h>
struct Student {
int id;
int age;
char * name;
};
int main() {
//类型需要写为struct Student,后面就是变量名称
struct Student s = {1, 21, "Odin"}; //结构体包含多种类型的数据,只需要把这些数据依次写好放在花括号里面就行了
}
struct Student {
int id;
int age;
char * name;
} s; //也可以直接在花括号后面写上变量名称(多个用逗号隔开),声明一个全局变量
当然,结构体的初始化需要注意:
struct Student s = {1, 21}; //如果只写一半,那么只会初始化其中一部分数据,剩余的内容相当于没有初始值,跟数组是一样的
struct Student s = {1, .name = "Odin"}; //也可以指定去初始化哪一个属性 .变量名称 = 初始值
那么现在我们拿到结构体变量之后,怎么去访问结构体内部存储的各种数据呢?
printf("id = %d, age = %d, name = %s", s.id, s.age, s.name); //结构体变量.数据名称 (这里.也是一种运算符) 就可以访问结构体中存放的对应的数据了
是不是很简单?当然我们也可以通过同样的方式对结构体中的数据进行修改:
int main() {
struct Student s = {1, 21, "Odin"};
s.name = "ice";
s.age = 22;
printf("id = %d, age = %d, name = %s", s.id, s.age, s.name);
}
结构体数组和指针
如果我们需要保存很多个学生的信息,需要使用结构体类型的数组来进行保存:
#include <stdio.h>
struct Student {
int id;
int age;
char * name;
};
int main() {
struct Student arr[3] = {{1, 18, "Odin"}, //多个结构体数据用逗号隔开
{2, 17, "ice"},
{3, 18, "cloud"}};
}
那么现在如果我们想要访问数组中第二个结构体的名称属性,该怎么做呢?
int main() {
struct Student arr[3] = {{1, 18, "Odin"}, //多个结构体数据用逗号隔开
{2, 17, "ice"},
{3, 18, "cloud"}};
printf("%s", arr[1].name); //先通过arr[1]拿到第二个结构体,然后再通过同样的方式 .属性名称 就可以拿到对应的值了
}
当然,除了数组之外,我们可以创建一个指向结构体的指针。
int main() {
struct Student student = {1, 21, "Odin"};
struct Student * p = &student; //同样的,类型后面加上*就是一个结构体类型的指针了
}
我们拿到结构体类型的指针后,实际上指向的就是结构体对应的内存地址,和之前一样,我们也可以通过地址去访问结构体中的数据:
int main() {
struct Student student = {1, 21, "Odin"};
struct Student * p = &student;
printf("%s", (*p).name); //由于.运算符优先级更高,所以需要先使用*p得到地址上的值,然后再去访问对应数据
}
不过这样写起来太累了,我们可以使用简便写法:
printf("%s", p->name); //使用 -> 运算符来快速将指针所指结构体的对应数据取出
我们来看看结构体作为参数在函数之间进行传递时会经历什么:
void test(struct Student student){
student.age = 19; //我们对传入的结构体中的年龄进行修改
}
int main() {
struct Student student = {1, 18, "Odin"};
test(student);
printf("%d", student.age);
}
可以看到在其他函数中对结构体内容的修改并没有对外面的结构体生效,因此,实际上结构体也是值传递。我们修改的只是另一个函数中的局部变量而已。
所以如果我们需要再另一个函数中处理外部的结构体,需要传递指针:
void test(struct Student * student){ //这里使用指针,那么现在就可以指向外部的结构体了
student->age = 19;
}
int main() {
struct Student student = {1, 18, "小明"};
test(&student); //传递结构体的地址过去
printf("%d", student.age);
}
联合体
联合体也可以在内部定义很多种类型的变量,但是它与结构体不同的是,所以的变量共用同一个空间。
union Object { //定义一个联合体类型唯一不同的就是前面的union了
int a;
char b;
float c;
};
联合体非常奇妙:
#include <stdio.h>
union Object {
int a;
char b;
float c;
};
int main() {
union Object object;
object.a = 66; //先给a赋值66
printf("%d", object.b); //访问b
}
它们共用了内存空间,实际上我们先将a修改为66,那么就将这段内存空间上的值修改为了66,因为内存空间共用,所以当读取b时,也会从这段内存空间中读取一个char长度的数据出来,所以得到的也是66。
int main() {
union Object object;
object.a = 128;
printf("%d", object.b);
}
因为:128 = 10000000,所以用char读取后,由于第一位是符号位,于是就变成了-128。
联合体的大小至少是其内部最大类型的大小,这里是int所以就是4。当然联合体的其他使用基本与结构体差不多,这里就不提了。
枚举
我们来看一下枚举类型,枚举类型一般用于表示一些预设好的整数常量,比如我们风扇有低、中、高三个档位,我们总是希望别人使用我们预设好的这三个档位,而不希望使用其他的档位,因为我们风扇就只设计了这三个档位。
这时我们就可以告诉别人,我们的风扇有哪几个档位,这种情况使用枚举就非常适合。在我们的程序中,只能使用基本数据类型对这三种档位进行区分,这样显然可读性不够,别人怎么知道哪个代表哪个档位呢?而使用枚举就没有这些问题了:
/**
* 比如现在我们设计:
* 1 = 低档位
* 2 = 中档位
* 3 = 高档位
*/
enum status {low = 1, middle = 2, high = 3}; //enum 枚举类型名称 {枚举 = 初始值, 枚举...}
我们可以创建多个自定义名称的枚举,命名规则和变量差不多。我们可以当每一个枚举对应一个整数值,这样的话,我们就不需要去记忆每个数值代表的是什么档位了,我们可以直接根据枚举的名称来进行分辨
使用枚举也非常地方便:
enum status {low = 1, middle = 2, high = 3};
int main() {
enum status a = low; //和之前一样,直接定义即可,类型为enum + 枚举名称,后面是变量名称,值可以直接写对应的枚举
printf("%d", a);
}
int main() {
enum status a = high;
if(a == low) {
printf("低档位");
} else if (a == high){
printf("高档位");
} else {
printf("中档位");
}
}
当然也可以直接加入到switch语句中:
int main() {
enum status a = high;
switch (a) {
case low:
case high:
case middle:
default: ;
}
}
不过在枚举变量定义时需要注意:
enum status {low, middle, high}; //如果不给初始值的话,那么会从第一个枚举开始,默认值为0,后续依次+1
所以这里的low就是0,middle就是1,high就是2了。
如果中途设定呢?
enum status {low, middle = 6, high}; //这里我们给middle设定为6
这时low由于是第一个,所以还是从0开始,不过middle这里已经指定为6了,所以紧跟着的high初始值就是middle的值+1了,因此low现在是0,middle就是6,high就是7了。
typedef关键字
这里最后还要提一下typedef关键字,这个关键字用于给指定的类型起别名。
typedef int lbwnb; //使用方式:typedef 类型名称 自定义类型别名
比如这里我们给int起了一个别名,那么现在我们不仅可以使用int来表示一个int整数,而且也可以使用别名作为类型名称了:
#include <stdio.h>
typedef int lbwnb;
int main() {
lbwnb i = 666; //类型名称直接写成别名,实际上本质还是int
printf("%d", i);
}
typedef const char * String; //const char * 我们就起个名称为String表示字符串
int main() {
String str = "Hello World!";
printf(str);
}
当然除了这种基本类型之外,包括指针、结构体、联合体、枚举等等都可以使用这个关键字来完全起别名操作:
#include <stdio.h>
typedef struct test {
int age;
char name[10];
} Student; //为了方便可以直接写到后面,当然也可以像上面一样单独声明
int main() {
Student student = {18, "小明"}; //直接使用别名,甚至struct关键字都不用加了
}