指针篇章-(2)-(一级指针和二级指针之间的理解)

学习流程图

 ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

数组名的理解

术语

 在编程中,数组名通常是一个标识符,用于引用内存中的一段连续空间,这段空间被用来存储一系列具有相同数据类型的元素。数组名在不同的编程语言中可能有不同的含义或用途,但通常具有以下共同特点:
1. **地址标识**:数组名在大多数情况下可以看作是数组首元素的地址。在C语言中,数组名就是数组首地址的别名,这使得可以通过数组名直接访问数组的元素。
2. **类型指示**:数组名还表明了数组中元素的数据类型。例如,在C语言中,数组名 typename[] 表明它是一个 typename 类型的数组。
3. **维度表示**:在某些语言中,数组名还可以表明数组的维度。例如,`int arr[10][20]` 中的 `arr` 表明这是一个二维数组,有10行20列。
4. **语法结构**:在不同的编程语言中,数组名的使用可能有特定的语法规则。比如,在Python中,数组通常是通过列表来实现的,列表的名称不直接作为数组地址使用,而是作为对象引用。
5. **作用域和可见性**:数组名的作用域和可见性由其定义的位置和作用域规则决定。在函数外部定义的数组名可以在整个程序中使用,而函数内部定义的数组名通常只在该函数内部可见。
6. **指针操作**:在C和C++等语言中,数组名可以用作指向数组首元素的指针。这意味着可以通过指针算术操作访问数组元素。
7. **内存分配**:在某些语言中,数组名还与内存分配有关。例如,在C语言中,数组名用于分配连续的内存空间,以存储数组中的所有元素。
8. **语言特性**:不同的编程语言可能有不同的数组实现。例如,Java中的数组是对象,数组名在语法上更类似于引用类型。
9. **上下文相关**:数组名的含义还可能与其上下文有关。在某些语言的库函数中,数组名可能作为参数传递,以指示函数操作哪个数组。
了解和使用数组名的正确方式对于确保程序的正确性和性能至关重要。在实际编程中,需要根据具体的编程语言和开发环境来准确理解和使用数组名。

丛数值的角度看

丛数值的角度看 确实都一样

 丛地址的角度看

1.arr代表的是数组首元素的地址,当+1的时候会加一个元素的地址

2.&arr[0]代表的是数组首元素的地址,当+1的时候会加一个元素的地址也就是和arr是一样的

3.这里&arr表示取出数组首元素的地址,但是他本身代表的虽然是首元素的地址,但是当+1的时候他代表的是加上整个数组的地址也就是十个元素的地址 

也就是&(取地址取出的是整个数组的空间 ,但是开始指向的是首元素的地址)

这里又涉及到指针的类型,每次加减和这个类型有一定的关系

也和数组名有一定的关系

因为不同的数组名代表不同的含义

这里&arr表示取出数组首元素的地址,但是他本身代表的虽然是首元素的地址,但是当+1的时候他代表的是加上整个数组的地址也就是十个元素的地址 

代码举例分析 

arr代表的是数组首元素的地址,当+1的时候会加一个元素的地址

&arr[0]代表的是数组首元素的地址,当+1的时候会加一个元素的地址也就是和arr是一样的

———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 

下标引用操作符【】

术语

下标引用操作符是C语言中用于访问数组、函数和结构体元素的一种操作符。
在C语言中,数组是一个由相同类型元素组成的集合。数组下标引用操作符“[]”用于访问数组中的特定元素。数组的下标从0开始,所以对于一个n元素的数组,下标1对应的是数组的第二个元素,下标n对应的是数组的最后一个元素。需要注意的是,访问数组元素时,下标必须是一个整数,如果下标超出数组的范围,将会导致程序崩溃。
除了数组,下标引用操作符还可以用于访问结构体和联合体的成员。结构体是一种复合数据类型,它将多个不同类型的数据组合在一起。结构体成员访问操作符“.”和“->”用于访问结构体或结构体指针的成员。需要注意的是,访问结构体成员时,成员的名称必须与结构体定义中的名称相同。
另外,需要注意的是,在使用下标引用操作符时,程序会自动进行数组越界检查,如果访问的地址不合法,程序将会抛出异常。所以在编写程序时,需要确保访问的下标在数组或结构体的有效范围内。

举例

 也就是下标引用操作符的操作性是很大的

应该用指针接收

这里的arr已经表示整个数组 但是本质是指针 等同于进行指针sizeof的运算 本身就是不对的

数组传参形参可以写成数组形式

当然这个需要知道【10】这个数值可以不写,但是一般情况下,传参的情况下,往往会把这个数组的长度一起传参过来,减少冗余性

———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 

一级指针

 什么是一级指针

在C语言中,指针是一个变量,它存储了另一个变量的地址。指针的级别是指指针指向的变量类型是否为指针。一级指针,也称为指向基本类型的指针,是指向整型、浮点型、字符型等基本数据类型的指针。
一级指针的定义格式如下:
```c
数据类型 *指针变量名;
```
例如,如果我们想要创建一个指向整型的指针,我们可以这样定义:
```c
int *p;
```
在这里,`p` 是一个一级指针,它存储了一个整型变量的地址。
一级指针可以用来直接访问和修改它所指向的变量的值。例如:
```c
int var = 10;
int *p;
p = &var; // 将var的地址赋给指针p
*p = 20;  // 通过指针p修改var的值
```
在上面的例子中,`*p` 就是解引用操作符,它用于获取指针指向的地址处的值。
需要注意的是,一级指针直接指向的是基本数据类型的内存地址,而不是其他指针。如果你想要创建一个指向指针的指针,即二级指针,你需要定义一个指针类型为指针的数据类型:
```c
数据类型 **指针变量名;
```
例如:
```c
int *p1;
int **pp;
p1 = &var;
pp = &p1; // 将p1的地址赋给二级指针pp
```
在这里,`pp` 是一个二级指针,它存储了指向`p1`的指针的地址。

实例讲解

———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————— 

实参和形参

 术语的解释

在编程中,形参( formal parameter)和实参(actual argument)是函数调用过程中的两个重要概念。它们分别指的是函数定义中声明的参数和函数调用时实际传递的参数。
1. **形参(Formal Parameter)**:
   形参是在函数定义时声明的参数,它是一个变量,用于在函数内部接收传递给函数的值。形参在函数的作用域内是局部变量,它们只在函数执行期间存在。形参的类型和数量由函数定义决定。
   例如,在函数定义中:
   ```c
   void printNumber(int number) {
       printf("%d\n", number);
   }
   ```
   `int number` 就是形参,它定义了函数`printNumber`接受一个整数类型的参数。
2. **实参(Actual Argument)**:
   实参是在函数调用时实际传递给函数的值。实参可以是常量、变量或者表达式等任何能提供数据的实体。实参的类型必须与相应形参的类型相匹配,否则可能会发生类型错误。
   例如,在函数调用中:
   ```c
   printNumber(10);
   ```
   在这里,`10` 就是实参,它是在调用函数`printNumber`时传递给形参`number`的实际值。
在函数调用时,实参的值会被复制(按值传递)或者引用(按引用传递)到形参中,以便在函数内部使用。函数执行完毕后,形参的值恢复为初始值,实参的值则保持不变(除非实参是引用传递的情况)。
不同的编程语言有不同的参数传递机制,如按值传递、按引用传递、按共享引用传递等。这些机制决定了函数调用中形参和实参之间的关系和数据传递方式。

开房间举例

简单的举例就是,小敏今天开了一个房间是901,准备进行诈骗。然后让小帅第二天来,小帅第一天吧钱交出去之后,第二天来到这个酒店,说我要进去901,但是酒店说了,这个人退房了,已经没有这个房间了。

这个就是指针的寻址问题,

因为在另外一个函数里面进行运算的事情,只能返回一个数值,而不是整个地址,这个函数需要进行运算的时候,或者需要进行诈骗的时候会开辟一块空间,不需要进行诈骗会吧这个空间关闭。但是如果你还是需要回去那个空间,自然就找不到这个空间了。

简单的说就是,实参是实际的参数,形参是暂时的参数,main传递过去的参数是实参,形参在返回的时候会进行销毁,在指针(1)里面

指针篇章-(1)-CSDN博客

指针1篇章有一个举例,里面就讲解了这个关于开房间销毁问题,其实这个就是形参和实参的区别,形参在使用之后是会进行销毁的,最终只返回一个参数,也就是这个函数只是参与运算,最终返回一个结果,这个形参创建的函数空间最终是会进行销毁的。

举例

我将使用文本形式的ASCII艺术来模拟实参和形参的传递过程。
请注意,这里的ASCII艺术仅用于说明概念,并不是真正的编程图形表示。
假设我们有一个简单的函数 `add`,
它接受两个整数作为形参 `a` 和 `b`,然后返回它们的和。

// 函数定义
int add(int a, int b) {  //这里计算完 之后开辟的空间是会进行销毁的
    return a + b;
}
```
现在,在函数调用时,我们会有两个实参 `10` 和 `5`,我们希望将它们传递给 `add` 函数。
```c
// 函数调用
int result = add(10, 5);
```
使用ASCII艺术来表示这个过程:
```
函数定义:
int add(int a, int b) {
    return a + b;
}
函数调用:
实参1: 10
实参2: 5
形参1: a = 10
形参2: b = 5
返回值: a + b = 15
```
在这个例子中,实参 `10` 和 `5` 被传递给形参 `a` 和 `b`。函数 `add` 执行后,返回 `15`,这个返回值被赋给实参 `result`。
请注意,这只是一个非常简单的例子,实际的参数传递机制可能会更复杂,取决于编程语言的参数传递方式(如按值、按引用等)。

增加的注释

在函数调用中,实参是传递给函数的具体值或者数据,而形参是函数定义时声明的变量,用于接收实参的值。形参在函数内部使用完成后,随着函数执行结束,其占用的内存空间会被释放,即“销毁”。
指针在C语言中是一个非常重要的概念,它存储了变量的地址。当使用指针作为函数的形参时,实参传递给函数的是地址值,而不是实参本身。这意味着在函数内部,可以通过指针访问和修改实参所指向的数据。指针的使用可以避免复制数据,提高效率,但也需要注意内存管理,避免出现野指针等问题。
形参在函数执行完毕后,其所占用的内存空间会被释放。如果形参是通过指针传递的,那么释放的是实参所指向的内存空间,而不是形参本身的内存空间。
在C语言中,如果实参是动态分配的内存(如使用`malloc`分配的空间),那么在函数调用结束后,如果形参是按值传递的,实参所指向的内存空间需要由程序员手动释放,否则可能会导致内存泄露。如果形参是按引用传递的,那么实参所指向的内存空间会在函数内部被操作,但不会被自动释放,仍需程序员在合适的时候释放。
在const篇章中,讨论的可能是关于const关键字的使用,它用于限定变量,使其不能被修改。在函数传参时,如果将const关键字用于形参,那么在函数内部将不能修改实参的值。这是为了保护实参的数据不被意外修改,增加了函数的健壮性。
总之,实参和形参的区别在于它们的生命周期和作用域,以及它们在函数调用过程中的数据传递方式。正确使用指针和const关键字等语言特性,可以帮助程序员更有效地管理和使用内存资源。

 ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

一维数组传参

什么是数组传参 

数组传参是指在编程中,将数组作为参数传递给函数的过程。当一个函数需要操作一个数组时,它可以通过参数接收一个数组,以便在函数内部访问和处理数组的数据。
在不同的编程语言中,数组传参的方式可能有所不同,但基本概念是相似的。通常,数组传参涉及到以下几个方面:
按值传递(Pass by Value):在一些语言中,数组传参实际上是传递数组的副本或指针的副本。这意味着在函数内部对数组的修改不会影响原始数组。
按引用传递(Pass by Reference):在另一些语言中,数组传参是通过传递数组的指针或引用进行的。这意味着函数内部对数组的修改会直接影响原始数组。
传递数组长度:在某些语言中,如C和C++,函数需要知道数组的长度。因此,除了数组本身,还需要传递数组的长度作为参数。
高级语言的简化:在高级编程语言中,如Python、Java或C#,数组传参通常更为简便。这些语言会自动处理数组的大小,因此不需要显式传递长度。

举例

#include <stdio.h>

void printArray(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main() {
    int myArray[] = {1, 2, 3, 4, 5};
    int arraySize = sizeof(myArray) / sizeof(myArray[0]);

    printArray(myArray, arraySize); // 将数组和它的长度传递给函数

    return 0;
}

在这个例子中,printArray 函数接受一个整数数组arr和一个整数size作为参数。在main函数中,我们创建了一个整数数组myArray,并计算了它的长度。然后,我们将myArray和它的长度作为参数传递给printArray函数。函数接收到的参数实际上是数组的一个副本和它的长度,函数内部的操作不会改变原始数组。

小结

如果是这个传参传的是&,此时应该用指针接收

如果是传参不是&而是数组的类型,此时也就是下标引用操作符接收

这里的arr已经表示整个数组 但是本质是指针 等同于进行指针sizeof的运算 本身就是不对的

传参的举例

数组传参的举例

数组传参形参可以写成数组形式

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

冒泡排序 

图解 

tmp图解 

内容图解 

每次循环的次数减少

 for循环详解

冒泡排序是一种简单的排序算法,它重复地遍历要排序的数列,
一次比较两个元素,如果它们的顺序错误就把它们交换过来。
遍历数列的工作是重复地进行直到没有再需要交换,
也就是说该数列已经排序完成。
在冒泡排序中,外层循环负责控制遍历的轮数,
而内层循环负责在每一轮中进行相邻元素的比较和交换。
通常,冒泡排序的代码如下所示:

for (i = 0; i < sz-1; i++) {
    for (j = 0; j < sz - i - 1; j++) {
        if (arr[j] > arr[j + 1]) {
            // 交换arr[j]和arr[j+1]
        }
    }
}
这里的`sz`是数组`arr`的长度。
现在我们来解释为什么内层循环的`j`需要满足`j < sz - i - 1`。

在每一轮的外层循环中,最大的元素会被放到数组的最后位置。
因此,在下一轮循环开始时,我们不需要再检查已经排好序的部分。
这意味着,对于外层循环的每一次迭代`i`,我们只需要在内层循环中检查前`sz - i - 1`个元素。
例如,如果数组的长度是`5`,那么:

 当`i = 0`时,我们需要比较所有`5`个元素,因此`j`的范围是`0`到`4`。
当`i = 1`时,最大的一个元素已经在数组的最后一个位置,所以我们只需要比较前`4`个元素,`j`的范围是`0`到`3`。
- 当`i = 2`时,两个最大的元素已经在数组的最后两个位置,我们只需要比较前`3`个元素,`j`的范围是`0`到`2`。
- 以此类推。
如果内层循环使用`j < sz - 1`,那么在`i = 1`时,`j`的范围将是`0`到`4`,这是不必要的,因为我们知道`arr[4]`已经是排序好的了。同样,如果内层循环使用`j < sz`,那么在`i > 0`时,它将检查已经排好序的元素,这也是不必要的。
因此,`j < sz - i - 1`确保了每次外层循环只比较和交换还未排好序的元素,这使得冒泡排序更加高效。

for循环画图详解

下面我用一个简单的图来解释冒泡排序中内层循环的`j`为什么需要满足`j < sz - i - 1`。
假设我们有一个数组`arr` = `[4, 2, 9, 1, 5]`,它的长度是`sz = 5`。我们将通过冒泡排序对这个数组进行排序。
在第一轮外层循环中,`i = 0`,我们需要比较所有元素,所以内层循环的`j`范围是`0`到`4`。在这个过程中,最大的元素`9`会被放到数组的最后一个位置。
在第二轮外层循环中,`i = 1`,最大的元素`9`已经在数组的最后一个位置,所以我们只需要比较前`4`个元素。内层循环的`j`范围现在是`0`到`3`。
在第三轮外层循环中,`i = 2`,两个最大的元素`9`和`5`已经在数组的最后两个位置,我们只需要比较前`3`个元素。内层循环的`j`范围是`0`到`2`。
在第四轮外层循环中,`i = 3`,三个最大的元素`9`、`5`和`4`已经在数组的最后三个位置,我们只需要比较前`2`个元素。内层循环的`j`范围是`0`到`1`。
在第五轮外层循环中,`i = 4`,四个最大的元素`9`、`5`、`4`和`2`已经在数组的最后四个位置,我们只需要比较前`1`个元素。内层循环的`j`范围是`0`到`0`。在这一轮中,内层循环不会执行任何交换,因为`arr[0]`已经是在正确的位置上。
如果没有`sz - i - 1`这个条件,内层循环会在`i > 0`时继续检查已经排好序的元素,这是不必要的。正确的条件`j < sz - i - 1`确保了每次外层循环只比较和交换还未排好序的元素。
下面是一个简化的图示,展示了这个过程:
``` 


初始数组: [4, 2, 9, 1, 5]
第一轮后: [2, 4, 9, 1, 5] (9移到末尾)
第二轮后: [2, 4, 1, 5, 9] (9和5移到末尾)
第三轮后: [2, 1, 4, 5, 9] (9、5和4移到末尾)
第四轮后: [1, 2, 4, 5, 9] (9、5、4和2移到末尾)
第五轮后: [1, 2, 4, 5, 9] (数组已经排序完成)

代码

//#define _CRT_SECURE_NO_WARNINGS 1
//#include<stdio.h>
从小到大排序 版本1
//void effervescence(int arr[], int sz)
//{
//	for (int i = 0; i < sz - 1; i++)
//	{
//		for (int j = 0; j < sz - 1; j++)
//		{
//			if (arr[j] > arr[j + 1])
//			{
//				int tmp = 0;
//				tmp = arr[j];
//				arr[j] = arr[j + 1];
//				arr[j + 1] = tmp;
//			}
//		}
//	}
//}
//void Print(int arr[], int sz)
//{
//	for (int i = 0; i < sz; i++)
//	{
//		printf("%d ", arr[i]);
//	}
//}
//int main()
//{
//	int arr[] = { 2,3,4,1,6,5,8,7,9,0,10 };
//	int sz = sizeof(arr) / sizeof(arr[0]);
//	//排序
//	effervescence(arr, sz);
//	//打印
//	Print(arr, sz);
//	return 0;
//}

//版本2
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
//从小到大排序 指向同一个空间
void effervescence(int* arr, int sz)
{
	for (int i = 0; i < sz - 1; i++)
	{
		for (int j = 0; j < sz - 1 - i; j++)
		{
			if (arr[j] > arr[j + 1])
			{
				int tmp = 0;
				tmp = arr[j];
				arr[j] = arr[j + 1];
				arr[j + 1] = tmp;
			}
		}
	}
}
int main()
{
	int arr[] = { 2,3,4,1,6,5,8,7,9,0,10 };
	int sz = sizeof(arr) / sizeof(arr[0]);
	int* p = arr;
	//排序
	effervescence(arr, sz);
	//打印
	for (int i = 0; i < sz; i++)
	{
		printf("%d ", *(p + i));
	}
	return 0;
}

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

二级指针

二级指针的解释

在C或C++编程语言中,二级指针是指向指针的指针,即它存储的是另一个指针的地址。简而言之,二级指针让我们能够间接地通过两个层次的指针来访问内存。
为了更好地理解二级指针,让我们通过一个例子来说明:
假设我们有一个整数数组 `int arr[10];`,我们可以使用一级指针(通常是数组名 `arr`)来直接访问数组中的元素,例如 `arr[0]`。
如果我们想要使用指针来访问数组元素,我们可以声明一个指针 `int *p;`,然后将 `p` 指向数组的第一个元素,即 `p = arr;`。这样,通过一级指针 `p`,我们可以访问数组 `arr` 的元素,如 `*p` 或 `p[0]`。
现在,如果我们想要使用另一个指针来指向这个一级指针 `p`,我们可以声明一个二级指针 `int **pp;`,并将 `pp` 指向 `p`,即 `pp = &p;`。这样,通过二级指针 `pp`,我们可以访问一级指针 `p`,进而访问数组 `arr` 的元素。例如,`**pp` 或 `pp[0]`。
总结一下,二级指针的使用场景包括:
1. 函数参数:当你需要传递指针的地址给函数时,例如动态分配内存。
2. 指针数组:存储指针的数组,每个元素是一个指针。
3. 函数返回多个指针:在C中,函数可以返回多个指针值。
正确使用二级指针对于深入理解和高效使用C或C++语言至关重要,尤其是在涉及内存管理和动态分配时。

一级指针和二级指针的区别

 二级指针指向的一级指针指向的空间

一级指针指向的是 a字符所创建的空间

这里是取地址(也就是取出)a的地址,存放到一级指针里面

简单的说就是 这里5个地址

a本身具备一个空间 

*p本身具备一个空间 *p存储a的空间

 同理

*p本身具备一个空间 *p存储 指向a的空间、

*pp本身具备一个空间 *pp存储 指向*p存储的空间

这里举例的  二级指针 是int*类型的

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

指针数组

整形数组 存放整形的数组

字符数组 存放字符的数组

指针数组 存放字符的数组

指针数组是一个数组,其中的每个元素都是指针类型。简单来说,它是一个存储指针的数组。

定义一个指针数组的语法

<数据类型> *<数组名>[<数组大小>];

其中,<数据类型>表示指针所指向的数据类型,<数组名>是数组的名称,<数组大小>表示数组的大小。

指针数组可以存储多个指针,每个指针可以指向不同的内存地址。这使得指针数组非常灵活,可以用于存储不同类型的数据或者不同对象的地址。

例如,定义一个指针数组来存储整型指针:

int *ptrArray[5];

这个指针数组可以存储5个整型指针。每个元素都可以指向一个整型变量或者整型数组。

使用指针数组时,可以通过索引来访问和操作每个指针元素。例如,可以通过ptrArray来访问第一个指针元素,并通过*ptrArray来访问该指针所指向的值。

总结一下,指针数组是一个存储指针的数组,它提供了一种灵活的方式来存储和操作不同类型的指针

数组名

需要注意,指针数组需要最数组名有深刻的理解

可以阅读一下这一篇文章

sizeof和strlen的详细万字解读-CSDN博客

下面进行指针数组的举例

 ———————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

指针数组模拟二维数组

画图解析指针模拟的二维数组 

这里设置三个一维数组 

 外循环是三层一维数组,但是需要知道的是 数组里面的首元素是arr[0] 也就是从0开始计算的

所以 三行 其实也就是arr[2]

内层循环也就是这个每个一维数组里面有五个元素 所以循环的条件是小于5 这里不能等于 因为是从0开始的,一旦等于 ,会造成指针越界行为的发生

越界行为就比如,如下

这里,使用两个指针模拟二维数组。第一个指针指向数组的首地址,第二个指针指向数组的第一个元素。

当需要访问数组中的其他元素时,只需将第二个指针指向下一个元素即可。

这种方法可以节省内存空间,因为只使用了两个指针,而不是一个数组和一个指针来表示整个数组。 

 指针的解应用其实和这个下标引用操作符的解释性行为是差不多一样的

如下举例

parr[i]是访问parr数组的元素,parr[i]找到的数且元素指向了整型一维数组,parr[i][j]就是整型一维数组中的元素。
上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的。

代码举例 

在C或C++语言中,二维数组可以有多种实现方式,其中一种常见的方式是使用指针来模拟二维数组。
这种方法在内存中并不实际存储一个真正的二维数组结构,而是通过指针的巧妙运用来达到类似的效果。

假设我们有一个二维数组 `int arr[3][4];`,它有3行4列,总共12个整数。在C语言中,这个数组的元素是这样存储的:

arr[0][0] -> arr[0][1] -> arr[0][2] -> arr[0][3]
   ^               |               |             |
   |               |               |             |
arr[1][0] -> arr[1][1] -> arr[1][2] -> arr[1][3]
   ^               |               |             |
   |               |               |             |
arr[2][0] -> arr[2][1] -> arr[2][2] -> arr[2][3]


每一行的元素在内存中是连续存储的。如果我们想要通过指针来访问这个数组的元素,我们可以这样做:

int *row = arr[0]; // 将第0行的首地址赋给指针row
int *p = row;      // p现在指向arr[0][0]
for (int i = 0; i < 4; ++i) {
    printf("%d ", *p); // 输出arr[0][i]的值
    p++;               // p指向下一列的元素
}
printf("\n");


如果我们想要模拟一个二维数组,我们可以自己手动分配内存来模拟这种结构
例如:

#include <stdio.h>
#include <stdlib.h>
int main() {
    int rows = 3;
    int cols = 4;
    int *matrix = (int *)malloc(rows * cols * sizeof(int));
    if (matrix == NULL) {
        fprintf(stderr, "Memory allocation failed\n");
        return 1;
    }
    // 初始化matrix指向的内存为0
    for (int i = 0; i < rows * cols; ++i) {
        matrix[i] = 0;
    }
    // 通过指针模拟访问二维数组的元素
    int *p = matrix; // p指向数组的第一个元素
    for (int i = 0; i < rows; ++i) {
        for (int j = 0; j < cols; ++j) {
            // *(p + i * cols + j) = ...; // 设置元素值
            printf("%d ", *p); // 输出当前元素值
            p++;               // p指向下一个元素
        }
        printf("\n");
        p += (cols - 1); // 跳到下一行的第一个元素
    }
    free(matrix); // 释放内存
    return 0;
}

在这个例子中,我们使用`malloc`分配了一个一维数组,然后通过指针操作来模拟二维数组的访问。
注意,这里的`p`指针每次都要加上`cols`或`(cols - 1)`来移动到正确的位置,因为我们是手动模拟二维数组的布局。
通过这种方式,我们可以使用指针来模拟二维数组的各种操作,包括访问、设置和遍历元素。
这种方法在某些情况下非常有用,尤其是在需要动态分配内存或者需要显式控制内存布局的情况下。

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

字符指针变量

什么是字符指针变量

  字符指针变量是一种特殊的指针变量,它指向的是字符串常量或字符数组。字符指针变量可以用来存储字符串的地址,并使用字符串操作符进行字符串的读写操作。

在C语言中,字符串实际上是以字符数组的形式存储的,而字符指针变量则可以用来指向这个字符数组的首地址。通过字符指针变量,我们可以方便地对字符串进行操作和处理。

字符指针变量的声明方式为:char *ptr; 这里的ptr是一个指针变量,它可以指向一个字符或者字符数组。

通过字符指针变量,我们可以进行一系列的操作,例如:

  1. 字符串的赋值:可以通过将一个字符数组的首地址赋给字符指针变量来实现字符串的赋值。
  2. 字符串的输出:可以通过循环遍历字符指针变量指向的字符数组,并逐个输出字符来实现字符串的输出。
  3. 字符串的拷贝:可以使用strcpy函数将一个字符串拷贝到另一个字符串中。
  4. 字符串的比较:可以使用strcmp函数比较两个字符串是否相等。

strlen

指针篇章-(1)-CSDN博客

这里不对strlen进行解释 在指针(1)里面 已经对strlen进行解释

总的来说就是,strlen遇见‘\0’之前 都是会进行计算的

像下面的char arr[]={"asdf"};这个asdf后面是会藏一个字符串0的

字符指针变量的举例

这里解释一下 

这里之所以不不行 是因为在C语言里面 

常量是不能被修改的

常量是在程序中定义的值,其值在程序运行期间不能被改变。

在C语言中,使用`const`关键字来声明常量。常量的值必须在定义时确定,或者在初始化时赋值,且一旦赋值后,其值就不能被更改。
例如:


const int max_value = 100; // 声明一个常量,类型为整型,值为100
// max_value = 200; // 错误!不能修改常量的值


在这个例子中,`max_value`是一个常量,它的值被初始化为100,并且在整个程序中都不能被改变。如果尝试修改`max_value`的值,编译器将会报错。
常量的好处是它们可以提高程序的可读性和可维护性,同时也能够防止无意中修改重要数据。此外,在某些情况下,编译器可以为常量进行优化,因为它们在程序运行时的值是固定的。

也就是常量等同于在指针*左侧加上const 

const在指针(1)里面有详细解释

指针篇章-(1)-CSDN博客

所以注释这里 这里p2=‘w’是错误的 

不加const也不能修改

就跟能不能抢银行一样 肯定不行 门口放个保安 银行还是不能抢

const就是保安

打印字符不需要解引用

常量字符串本身就不能进行修改

常量字符串和 变量字符串

 常量字符串和变量字符串是在编程中常见的两种字符串类型,它们有以下区别:

  1. 值的不可变性:常量字符串的值是不可变的,一旦被定义,就不能修改。而变量字符串的值是可变的,可以随时修改。
 常量字符串1:
const char* constantString = "Hello, World!";

在上述代码中,“Hello, World!” 是一个常量字符串,其值不可修改。
变量字符串1:
char variableString[] = "Hello, World!";

在上述代码中,variableString 是一个变量字符串,其值可以修改。

存储方式:常量字符串通常存储在只读内存区域,而变量字符串通常存储在可读写的内存区域。
常量字符串2:
const char* constantString = "Hello, World!";

在上述代码中,“Hello, World!” 是一个常量字符串,它通常存储在只读内存区域。
变量字符串2:
char variableString[] = "Hello, World!";

在上述代码中,variableString 是一个变量字符串,它通常存储在可读写的内存区域。

内存分配方式:常量字符串通常在编译时就被分配内存空间,而变量字符串通常在运行时动态分配内存空间。
常量字符串3:
const char* constantString = "Hello, World!";

在上述代码中,“Hello, World!” 是一个常量字符串,它在编译时就被分配了内存空间。
变量字符串3:
char variableString[] = "Hello, World!";

在上述代码中,variableString 是一个变量字符串,它在运行时动态分配了内存空间。

常量字符串和 变量字符串地址对比

首先 很显然是不相同的

这里开始做出解释 

不想相同的根本原因是

在str1和str2里面  是开辟两个空间

但是在3 4 里面是指针指向的一个空间 就算后期修改 也是在一个空间进行修改 

这里解释一下 就是 因为字符串的内容是一样的

也就是说

在C语言中,字符串通常是由字符数组实现的,即一串连续的字符。当你有两个字符串指针,它们指向的内容是一样的,这意味着它们都指向了存储相同字符序列的内存地址。然而,这并不意味着两个指针指向的空间是一样的。

在这个例子中,str3和str4指向的内容是一样的,因为它们都指向了同一个字符串字面量"hello"。但是,str1和str2本身是两个不同的指针变量,它们在内存中占据不同的位置。当你打印它们的地址时,你会发现它们是不同的。
指针的值是内存地址,而指针指向的内容是存储在那个地址中的值。即使两个指针指向的内容相同,它们指向的空间(即指针变量的内存地址)也是不同的。这是指针和引用之间的一个重要区别:在C++中,引用是与目标数据同生的别名,所以如果两个引用指向同一个对象,它们引用的实际上是同一个内存位置。但在C语言中,指针只是指向内存地址的变量,它们可以被赋予相同的值,但它们本身是独立的。

这两不是同一个首元素地址

因为开辟的不是一个空间

图解 

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

结构体

什么是结构体

结构体是一种用户自定义的数据类型,可以组合多个相关值成为一个单一类型。它是由一批数据组合而成的结构型数据,结构体可以包含多个不同类型的字段,如基本数据类型、其他结构体、枚举类型等。在Rust中,结构体有着重要的作用,可以创建更复杂的数据结构,并定义它们的行为。结构体使用struct关键字定义,如`struct Person { name: String, age: i32, }`。结构体实例的创建需要使用构造函数。结构体字段可以是可变的,也可以是不可变的,默认情况下,结构体字段不可变。如需修改结构体字段,需要使用mut关键字。结构体也可以绑定方法,方法允许你以面向对象的方式操作结构体实例。结构体还提供了一种更新语法,使用..运算符(也称为点运算符或展开运算符)。结构体在不同的编程语言中都有类似的实现,如C++和Rust等。 

结构体的解释

结构体(Structure)是C语言中一种组织多个变量的方式,它允许我们将不同的数据类型组合在一起,形成一个单一的实体。
结构体在内存中占用的空间等于其所有成员占用的空间之和。
在C语言中,结构体是通过关键字 `struct` 定义的
下面是一个简单的结构体定义的例子:

struct Person {
    char name[50];
    int age;
    float height;
};

在这个例子中,我们定义了一个名为 `Person` 的结构体,它包含三个成员:`name`(字符数组,用于存储姓名),`age`(整型,用于存储年龄),和 `height`(浮点型,用于存储身高)。
要使用这个结构体,我们可以声明一个 `Person` 类型的变量:

struct Person p1;

然后,我们可以像访问普通变量一样访问 `p1` 的成员:

p1.name = "Alice";
p1.age = 30;
p1.height = 165.5;

我们也可以通过指针来访问结构体的成员,这可以提高代码的灵活性:

struct Person *p2;
p2 = &p1;
printf("%s\n", p2->name); // 输出: Alice

在上述代码中,`p2` 是一个指向 `Person` 结构体的指针。通过 `->` 操作符,我们可以访问它指向的结构体的成员。
结构体在实际编程中的应用非常广泛,例如,它可以用来表示现实世界中的对象或实体,如学生、员工等,每个实体都有其相应的属性。结构体也可以用来组织数据,使得数据管理更加方便和高效。

结构体的基本知识

结构是值的变量

也就是值的集合,这些集合称之为成员变量

数组一组相同元素的集合

结构体是一组不一定相同类型的元素的集合

复杂的对象不能通过简单的内置类型直接描述和表示 此时就有了结构体 来描述复杂类型

结构体的声明

但是需要知道的是,在函数体里面写的名字 ,如果要这个在其他函数进行使用,要么使用函数声明,要么把结构体放到最上面。

因为C语言的运行程序是从上往下进行运行的,但是进行函数的声明之后,就会先进行一次程序走一遍,再继续运行。

struct tag是名字//tag是名字 结构体是本身是不需要进行头文件的

{

}

大括号里面是成员 可以是多个 也可以是0个

最后是变量列表

举例 描述一个学生

一个汉字两个字符串

这个就是结构体类型

结构体的初始化

有了类型 才可以初始化

此时也就完成了结构体的初始化

这个是在main函数里面的初始胡

当然这个不仅可以在main函数里面进行初始胡 ,也可以在结构体下面的函数连进行初始话

初始化的时候就是 struct + 函数体名字(Stu) 在主函数里面 这个时候就再加上一个名字 arr等任意名字 就按照数组的方式可以进行初始化

也就是 可以是struct + Stu+si;

也可以是struct + Stu+arr;在主函数里面

在C语言中,结构体的初始化可以通过几种方式来完成,包括逐字段初始化、使用结构体数组、使用`malloc`分配内存后初始化,以及使用`memset`或`bzero`对内存块进行初始化。

1.
逐字段初始化
逐字段初始化是最直接的方法,直接为结构体的每个字段赋值例如:
struct Person {
    char name[50];
    int age;
    float height;
};
struct Person p1 = {"张三", 30, 165.5f};//按照顺序 也就是 name=张三,age=年龄,heihgt=身高



2.
 使用结构体数组
如果你有一系列结构体实例,你可以使用结构体数组来初始化它们:

struct Person students[3] = {
    {"Bob", 22, 175.0f},
    {"Charlie", 24, 180.0f},
    {"David", 23, 172.0f}
};


3.
使用`malloc`分配内存后初始化
如果你需要在程序运行时动态分配结构体的内存,你可以使用`malloc`函数,然后手动为每个字段赋值:
struct Person *p2 = (struct Person *)malloc(sizeof(struct Person));
if (p2 != NULL) {
    strcpy(p2->name, "Bob");
    p2->age = 22;
    p2->height = 175.0f;
}


4.
使用`memset`或`bzero`初始化
在某些情况下,你可能需要初始化整个结构体或结构体数组的全部字段,这时可以使用`memset`或`bzero`函数。`memset`将内存中的字节设置为指定的值,而`bzero`只是将内存中的字节设置为0(清零)。
struct Person p3;
memset(&p3, 0, sizeof(struct Person)); // 将p3的字段全部初始化为0
// 或者使用bzero
bzero(&p3, sizeof(struct Person)); // 将p3的字段全部初始化为0
请注意,使用`memset`或`bzero`时,要确保传递的地址是指向结构体的指针,而不是结构体本身。
这些是结构体初始化的常见方法。根据具体的需求和场景,你可以选择最适合你的初始化方式。

结构体的类型和变量

比喻

这个是图纸和房子的关系

结构体就是图示 主函数里面的初始化和框架也就是开始建立房子

类型和变量的关系(局部变量和全局变量)

这里也就是char name[100]所以占用的空间大一点

int age是整形 占据四个字节或者八个字节

char sex[5]又比int大一点

有了类型之后在主函数里面创建s1

这里的s2 s3 s4 就是结构体变量 这三个是函数外面创建的 也就是全局变量 和s1一样 但是s1 是局部变量 但是s2,s3,s4是结构体的全局变量

代码举例

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct Stu
{
	char name[100];//这里看理解为名字 一个汉字占两个字符
	int age;//这里可以设置成整形 因为这里是名字的意思 
};
struct Stu s3[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };
struct Stu s4[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };

void test()
{
	struct Stu s2[4] = { {"张三",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };

}

int main()
{
	//此时如果是在main函数里面 或者在test();函数里面创建的结构体变量,此时是局部变量 也就是cs1 s2
	//如果是在结构体外部创建的变量 此时是全局变量 也就是 s3 s4
	struct Stu s1[4] = { {"张三",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };

	
	return 0;
}

typedef

 typedef的使用
在编程中,`typedef` 是一个关键字,用于为已存在的数据类型创建一个别名。这样做可以让代码更易于阅读和维护,特别是在处理复杂或者冗长的类型名称时。
例如,在 C 语言中,您可以使用 `typedef` 为标准数据类型如 `int` 创建别名:

typedef int INT32; // 将 int 类型重命名为 INT32

之后,您就可以使用 `INT32` 代替 `int` 来声明变量:

INT32 a, b; // 这里实际上是指 int 类型的变量

在不同的编程语言中,`typedef` 的作用和用法可能会有所不同,但核心概念是类似的,都是为了简化代码中对数据类型的引用。

typedef对变量重命名 但是需要知道 C语言里面 如果没有对结构体类型进行typedef,struct是不能省略的

 也就是如果存在typedef 结构体后面的stu是一个类型


例如,如果我们有一个结构体:

struct Student {
    char name[50];
    int age;
    float score;
};

我们可以使用 `typedef` 为这个结构体定义一个新的类型名称:

typedef struct Student stu;

这样,`stu` 就成为了 `struct Student` 的一个别名。之后,你就可以使用 `stu` 来声明这个结构体的变量:

stu s1;

这里,`s1` 是一个 `struct Student` 类型的变量,但使用了 `stu` 作为它的类型名称。
所以,如果你看到代码中有 `typedef struct Student stu;`,那么 `stu` 就是一个代表 `struct Student` 类型的别名。

或者 

结构体的成员访问

结构体是可以互相包含的

可以在上一个 结构体里面 定义一个新的 结构体类型

上面我们知道在结构体外面创建的结构变量的全局变量

但是其实在这个外面创建的全局变量也是可以直接进行初始化的 

在全局变量和局部变量里面的代码举例里面进行了举例

举例

在s1里面进行修改

初始化 并且给他一些数值

初始化 逐步初始化,

100 字符 空指针

struct S +名字s2 然后初始化 括号{} 和 数组的初始化有些类似

这里依旧是结构体的初始化的举例

选择初始化

.选择 中间       ,      隔开

.的方式找到成员

这里需要记住

1   .  是  结构成员访问操作符 .

2   ->是结构成员访问操作符 .

重复一下

1   .  是  结构成员访问操作符 .

2   ->是结构成员访问操作符 .

 这里打印的是这三个

如果是需要进行这个多个结构体的访问和打印 需要用上循环

struct的B sb进行初始化和成员访问

怎么放里面怎么拿出来

也就是如何打印出来 这里需要一一对应的方式打印出来,哪怕是进行循环打印,这个结构体里面的数值也要进行一一对应的方式进行打印

下面打印的是结构体B 初始化函数名sb 

打印的时候就是sb.ch//意思就是sb下的struct ch

同理打印结构体第二个数值 也就是sb.s.a//意思就是struct B 结构体创建的sb下的,struct B里面的struct S s。

数值的传递

这里是直接把结构体传参过去了 传到set_stu函数里面 在后期会用得上

但是记得,在传参的时候需要带上struct+名字 +初始化的名字 

进行拷贝

把张三拷贝到name里面去

strcpy是拷贝函数(记着就行)

包含头文件string.h

此时set_stu就成功的把函数拷贝过来了
语法形式记着就可以

strcmp的解释

在C语言中,`strcpy` 函数用于将一个字符串复制到另一个字符串中。它的原型定义在 `string.h` 头文件中。`strcpy` 函数的语法格式如下:

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

参数说明:
- `dest`:指向目标字符串的指针,即要复制字符串到的位置。
- `source`:指向源字符串的指针,即要复制的字符串。
`strcpy` 函数会复制 `source` 指向的字符串到 `dest` 指向的空间中,包括字符串结束符 `\0`。注意,目标字符串数组必须有足够的空间来容纳源字符串,否则可能会导致缓冲区溢出。
示例用法:

#include <stdio.h>
#include <string.h>
int main() {
    char dest[20];
    const char *source = "Hello, World!";
    // 将源字符串复制到目标字符串中
    strcpy(dest, source);
    printf("复制后的目标字符串: %s\n", dest);
    return 0;
}

在这个例子中,`strcpy` 函数将 "Hello, World!" 复制到 `dest` 数组中,然后程序打印出复制后的字符串。

回归主体 

打印结构体s

t.什么什么 ,这个t其实就是创建的形参可以理解为

但是此时是错误

因为结构体是需要明确指向哪个地址的

需要用指针进行接收,所以传参的时候需要取地址,因为指针指向的是地址

画图解析原因

此时需要解决需要传址

在指针(1)指针篇章-(1)-CSDN博客里面解释了什么是传址 什么是传值

&s

这样就是正确的

这里指向的不是打印是age和name而是struct stu里面的name和age

这里也需要取地址名字s 然后指向这个位置 打印出来 这样打印的才是正确 

升级版本 箭头 结构体成员

打印结构体的代码以及结构体传参

 不包含循环的打印

此时也就是指针指向的是首元素的地址 所以打印的时候也就是打印的首元素的地址

要是想循环打印出内容的情况下 只需要在外部加个for循环 

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct Stu
{
	char name[100];//这里看理解为名字 一个汉字占两个字符
	int age;//这里可以设置成整形 因为这里是名字的意思 
};
struct Stu s3[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };
struct Stu s4[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };

void test(struct Stu *ps)//这里调用的是main函数里面的结构体进行打印 
{
	printf("%s %d\n", ps->name, ps->age);
}

int main()
{
	//此时如果是在main函数里面 或者在test();函数里面创建的结构体变量,此时是局部变量 也就是cs1 s2
	//如果是在结构体外部创建的变量 此时是全局变量 也就是 s3 s4
	struct Stu s1[4] = { {"张三",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };
	test(&s1);
	struct Stu s2[4] = { {"zhangsan",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };
	test(&s2);

	return 0;
}

循环打印出结构体

#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
struct Stu
{
	char name[100];//这里看理解为名字 一个汉字占两个字符
	int age;//这里可以设置成整形 因为这里是名字的意思 
};
struct Stu s3[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };
struct Stu s4[4] = { {"张三",12} ,{"lisi",23}, {"王五",2}, {"二狗",1} };

void test(struct Stu *ps)//这里调用的是main函数里面的结构体进行打印 
{
	printf("%s %d\n", ps->name, ps->age);
}

int main()
{
	//此时如果是在main函数里面 或者在test();函数里面创建的结构体变量,此时是局部变量 也就是cs1 s2
	//如果是在结构体外部创建的变量 此时是全局变量 也就是 s3 s4
	struct Stu s1[4] = { {"张三",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };
	int sz1 = sizeof(s1) / sizeof(s1[0]);
	for (int i = 0; i < sz1; i++)
	{
		test(&s1[i]);
	}
	printf("\n");
	struct Stu s2[4] = { {"zhangsan",12 } ,{"lisi",23}, {"王五",2}, {"二狗",1} };
	int sz2 = sizeof(s2) / sizeof(s2[0]);
	for (int i = 0; i < sz2; i++)
	{
		test(&s2[i]);

	}

	return 0;
}

————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————————

 结构体传参 

传值


   当结构体作为函数参数时,如果使用传值方式,那么函数将接收一个结构体变量的副本。这意味着在函数内部对结构体的修改不会影响到原始的结构体变量。因为在函数调用时,会创建一个结构体的副本并传递给函数,函数操作的是这个副本,而不是原始数据。
   示例: 


   
   struct Student {
       char name[50];
       int age;
   };
   void printStudent(struct Student s) {
       printf("Name: %s, Age: %d\n", s.name, s.age);
   }
   int main() {
       struct Student student = {"Alice", 20};
       printStudent(student);  // 调用时传递的是student的副本
       return 0;
   }
 

传址


   当使用传址方式时,函数接收的是结构体变量的地址。这意味着在函数内部对结构体的修改会影响到原始的结构体变量,因为函数操作的是存储在原始地址中的数据。
   示例:
  


   struct Student {
       char name[50];
       int age;
   };
   void modifyStudent(struct Student *s) {
       strcpy(s->name, "Bob");  // 修改的是传入的结构体变量
       s->age = 21;
   }
   int main() {
       struct Student student = {"Alice", 20};
       modifyStudent(&student);  // 传递的是student的地址
       printf("Name: %s, Age: %d\n", student.name, student.age);  // 输出已修改的值
       return 0;
   }

总结

 在实际编程中,通常根据是否需要修改原始数据来选择使用传值还是传址。如果函数需要修改结构体数据,或者结构体较大,为了节省内存和提高效率,通常使用传址方式。如果函数只是读取结构体数据,而不进行修改,使用传值方式即可。

原因函数传参的时候 参数压栈 参数过大的时候 压栈就过大 不仅浪费空间 而且浪费时间

如果直接传递地址过去 无非就是四个或者八个字节 只要有一个指针大小的空间就够了

  • 39
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值