找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1545

积分

0

好友

233

主题
发表于 3 天前 | 查看: 2855| 回复: 0

一、一些最基础的概念

1、指针与数组的核心——地址

地址是理解指针与数组的核心,之后的所有内容也基本离不开地址的概念。每个地址都与每个字节绑定,是内存中唯一位置的标识符(每个字节存储单元都对应着唯一不重复的地址)。每个变量、常量或数组元素在计算机内存中都有一个特定的地址。内存由一系列存储单元组成,这些存储单元按照字节编号,每个存储单元都有唯一的地址。

当我们声明一个变量时,编译器会为该变量分配一块内存空间,并返回一个指向这块内存空间的地址。这个地址可以使用指针变量来保存和操作。

回顾一下之前的知识:

  • 计算机存储数据的基本单位:字节(Byte)
  • 计算机表示数据的基本单位:位(bit)
  • 表示数据的方式:二进制
  • 整型数据占4个字节
  • 1Byte = 8bit

根据上面的概述,我们不妨思考一下,为什么计算机中要有地址这个概念?为什么每个字节都要有一个地址?

我们可以做一个类比:把一栋楼当成一个字节存储单元,把每个门牌号当成一个地址。那么,计算机在存储数据时更准确的描述是:我要把数据放到0x00000001的地址空间里。

图片

那么我们如何获取这个地址呢?可以使用运算符&(取址符)来获取变量的地址,如int x; printf("%p", &x);会打印出变量x在内存中的地址。同时,你也可以使用指针变量来访问地址所对应的值,通过解引用操作*,如printf("%d", *p);将输出指针p所指向的整数值。

理解C语言中的地址概念对于高级编程技术(如动态内存分配、函数参数传递、数据结构实现等)至关重要。通过控制和操作内存地址,程序员可以更直接地管理程序的数据存储和访问,但也需要格外注意潜在的内存错误,如空指针引用、内存泄漏等问题。

#include <stdio.h>
int main(int argc, const char *argv[]){
 int x = 123;
 /* int *y = 456;
  *初始化指针 y 时将一个整数(456)强制转换为了指针类型。
  这是因为你试图直接将一个整数值赋给指针变量 y,而没有为它分配内存地址
  */
 int *y; //初始化一个指针变量
 y = &x; //将变量x的地址赋给变量y
 printf("&x = %p\n", (void *)&x);
 printf("&x = %p\n", &x); //%p:打印变量x在内存中的地址
 printf("&y = %p\n", y);
 /*
 printf("*y = %p\n", *y);
 这里会警告,是因为在printf()函数中,格式说明符%p需要一个指向内存地址的指针作为参数
 而这里传递了一个整型变量
 */
 printf("y = %p\n", (void *)y); //打印出指针变量y的值(也就是变量x的地址)
 return 0;
}

图片

以上就是关于取x的地址的几种方法。

2、什么是十六进制数字

我们熟悉的是十进制(逢十进一),计算机熟悉的是逢二进一(在电路中一般把其当成两个状态,即,电信号),那么十六进制也是能够与计算机进行直接交互、转化(但直接使用除外,逢十六进一,0~9、A~F)。

十六进制表示法的好处:

一个十六进制位等价于四个二进制位,比如,0000 = 01111 = F1101 = D……

  • 简化二进制转换:在计算机科学中,二进制是基本的计数系统。将二进制转换为十六进制比转换为十进制更容易。因为十六进制每4位可以对应一个十六进制数,而十进制需要8位才能对应一个十进制数。例如,二进制数1101转换成十六进制就是D,而转换成十进制则是13。
  • 编程中的便捷性:在编程语言中,十六进制常用于颜色代码、内存地址和文件格式等。例如,HTML和CSS颜色代码通常用六位十六进制数表示(如#FF0000代表红色)。此外,许多编程语言也支持以十六进制字面量的形式表示整数。
  • 数据存储和传输:在数据存储和传输过程中,十六进制也经常被使用。例如,MAC地址和IP地址通常采用十六进制表示,这使得它们更易于阅读和处理。
  • 计算机硬件和软件:在计算机硬件和软件开发中,十六进制也得到了广泛应用。例如,汇编语言和一些低级编程语言通常使用十六进制来表示指令和数据。
#include <stdio.h>
int main(int argc, const char *argv[]){
 int a = 0x6a;
 printf("a(10) = %d\n", a);
 printf("a(16) = %x\n", a); // 注意:%x表示输出十六进制
 printf("a(16) = %X\n", a); // 注意:这里%X是大写,那么对应的输出也会是大写
    int int_max = 0x7fffffff;  // 因为最大值是0111 1111,又因为0111转为十六进制是0x7,后面全是1,所以为f
 int int_min = 0x80000000;  // 因为最小值是1000 0000,又因为1000转为十六进制是0x8,后面全是0,所以为0
 printf("int_max = %d\n", int_max); //输出一个整型最大值
 printf("int_min = %d\n", int_min);
 printf("input hex:");
 scanf("%x", &a); //输入一个十六进制,在终端输入不需要加0x
 printf("a(10) = %d\n", a);
 printf("a(16) = %x\n", a);
 printf("a(16) = %X\n", a);
 return 0;
}
// 整型的最大值:首先第一位符号位肯定是0,表示正数,后面跟着31个1,即是最大值;
// 整型最小值:首先第一位符号位是1,表示负数,后面跟着31个0,即是最小值。

图片

3、地址到底是一个几位的二进制数据
#include <stdio.h>
int main(int argc, const char *argv[]){
 int a;
 double b;
 char c;
 float d;
 printf("sizeof(int &) = %u\n", sizeof(&a));
 printf("sizeof(double &) = %u\n", sizeof(&b));
 printf("sizeof(char &) = %u\n", sizeof(&c));
 printf("sizeof(float &) = %u\n", sizeof(&d));
 return 0;
}

图片

根据上面的实例,可以得出无论是什么数据类型,最终输出的结果都是一样的。这与什么有关呢?其实这与系统架构和操作系统有很大关系,这些地址用于表示内存中的位置。

目前来看。32位地址可以表示的最大内存大小是4GB(2^32字节,一个字节都对应着一个唯一的地址空间),而64位地址可以表示的内存大小远大于此(2^64字节)。由于实际可用的物理内存通常小于理论最大值,因此某些情况下可能会使用较小的地址空间。

二、数组的定义与使用

1、初识数组
#include <stdio.h>
int main(int argc, const char *argv[]){
 int i; //首先先声明一个i变量,因为我是c98的标准
 int a[i];
 for(i = 0; i < 10; i++)
 {
  a[i] = 2 * i; //在输出的基础上乘以2
  printf("a[%d] = %d\n", i, a[i]);
 }
 return 0;
}

图片

什么是可变长数组?
  • 首先,先读入一个数字n;
  • 然后,假设初始化一个长度为2n的数组;(也就是我们在定义数组时可以将一个表达式作为数组大小的i计算方法)
  • 最后,还是上面那一步,用一个for循环来遍历这个数组。
#include <stdio.h>
int arr1(){
 int i; //首先先声明一个i变量,因为我是c98的标准
 int a[i];
 for(i = 0; i < 10; i++)
 {
  a[i] = 2 * i; //在输出的基础上乘以2
  printf("a[%d] = %d\n", i, a[i]);
 }
 return 0;
}
int arr2(){
 int i;
 int n;
 printf("input:");
 scanf("%d", &n);
 int a[2*n];
 for(i = 0; i < 2*n; i++)
 {
  a[i] = 3 * i;
  printf("a[%d] = %d\n", i, a[i]);
 }
}
int main(int argc, const char *argv[]){
 //arr1(); //因为这是两个函数,所以务必要注释一个函数,还没有学习多线程,现在只可以两个函数互相调用,但还无法让两个函数同时运行
 arr2();
 return 0;
}
#include <stdio.h>
int main(int argc, const char *argv[]){
 int i;//声明一个变量i
 int x;
 // int a[i]; //这只是定义一个数组,没有赋初始值
 int a[10] = {1, 2, 3, 4, 5};//数组赋值,这里只有1~5,后面不够全部补0
 int b[10] = {0};
 int c[10] = {};
 int d[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 printf("sizeof(a)/sizeof(int) = %u\n", sizeof(a)/sizeof(int)); //sizeof(a)/sizeof(int):求这个数组a的大小
 printf("sizeof(b)/sizeof(int) = %u\n", sizeof(b)/sizeof(int));
 printf("sizeof(c)/sizeof(int) = %u\n", sizeof(c)/sizeof(int));
 printf("sizeof(d)/sizeof(int) = %u\n", sizeof(d)/sizeof(int));
 printf("\n");
 //for(i = 0; i < 10; i++) sizeof(a)/sizeof(a[0]) = 10
 //上行表达式就是计算整个数组占用的字节数除以单个元素占用的字节数,得到数组的元素数量
 for(i = 0; i < sizeof(a)/sizeof(a[0]); i++)
 {
  //a[i] = a[10]; //注意:如果这个数组已经赋值了,这一步不用写
  printf("a[%d] = %d \n", i, a[i]);
  //b[i] = b[10]; //上面定义了一个数组,这里是初始化值
  //printf("b[%d] = %d \n", i, b[i]);
  //c[i] = c[10];
  //printf("c[%d] = %d \n", i, c[i]);
 }
 return 0;
}

在这个例子中,我们定义了一个包含5个元素的数组a。然后,我们使用for循环来遍历数组中的每个元素。在循环体中,我们使用printf()函数打印出当前元素的索引和值。

注意,为了确定数组的大小,我们在循环条件中使用了sizeof(a) / sizeof(a[0])。这个表达式会计算整个数组占用的字节数除以单个元素占用的字节数,从而得到数组的元素数量。这样可以确保即使数组的大小改变,我们的代码也能正确地遍历所有元素。

#include <stdio.h>
int main(int argc, const char *argv[]){
 int i;//声明一个变量i
 int x;
 // int a[i]; //这只是定义一个数组,没有赋初始值
 int a[10] = {1, 2, 3, 4, 5};//数组赋值,这里只有1~5,后面不够全部补0
 size_t size = sizeof(a)/sizeof(int);//求数组a的大小(长度\容量)
 int b[10] = {0};
 int c[10] = {};
 int d[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
 //printf("sizeof(a)/sizeof(int) = %u\n", sizeof(a)/sizeof(int));
 printf("sizeof(a)/sizeof(int) = %u\n", size);
 printf("size = %zu\n", size);
 printf("a = %p\n", a);
 //sizeof(a)/sizeof(int):求这个数组a的大小
 printf("sizeof(b)/sizeof(int) = %u\n", sizeof(b)/sizeof(int));
 printf("sizeof(c)/sizeof(int) = %u\n", sizeof(c)/sizeof(int));
 printf("sizeof(d)/sizeof(int) = %u\n", sizeof(d)/sizeof(int));
 printf("\n");
 //for(i = 0; i < 10; i++) sizeof(a)/sizeof(a[0]) = 10
 //上行表达式就是计算整个数组占用的字节数除以单个元素占用的字节数,得到数组的元素数量
 //for(i = 0; i < sizeof(a)/sizeof(a[0]); i++)
 for(i = 0; i < size; i++)
 {
  //a[i] = a[10]; //注意:如果这个数组已经赋值了,这一步不用写
  printf("a[%d] = %p\n", i, &a[i]);
  //b[i] = b[10]; //上面定义了一个数组,这里是初始化值
  //printf("b[%d] = %d \n", i, b[i]);
  //c[i] = c[10];
  //printf("c[%d] = %d \n", i, c[i]);
 }
 return 0;
}

数组的掌握是学习指针的必经之路以及必要条件,所以给出的代码一定要多看、多练、多敲几遍。

size_t是C语言中的一种数据类型,它通常用于表示长度、大小或容量。size_t是一种无符号整数类型,它的具体大小和字节数取决于编译器和目标平台。

在标准库函数中,size_t经常被用作参数或返回值来表示内存区域的大小。例如,在malloc()函数中,你需要提供一个size_t类型的参数来指定要分配多少字节的内存空间。同样地,strlen()函数返回一个size_t类型的值,表示给定字符串的长度(不包括结束的空字符\0)。

使用size_t的好处之一是它可以确保足够的存储空间来表示任何可能的大小或长度,而不会因为使用较小的数据类型而导致溢出问题。此外,由于size_t与机器相关,它能够充分利用不同架构下的最大寻址能力。

size_t定义在<stddef.h>头文件中,如果你在代码中需要使用这个类型,你应该包含这个头文件:

#include <stddef.h>

然后就可以声明并使用size_t变量了:

size_t length;
length = strlen("Hello, World!");
printf("The length of the string is: %zu\n", length);

注意在使用%zu作为格式说明符来打印size_t类型的值。

2、关于数组的两个算法
2.1 素数筛算法

补充一个知识点(不多说,因为全在注释里面)

#include <stdio.h>
int main(int argc, const char *argv[]){
 short c = 0, d = 0;
 unsigned char a, b;
 //a = 'A';//ascii中,A=65 ...
 //b = 'B';//B=66
 a = 49;
 b = 50;
 c = a | b;
 //使用了按位或运算,并将值赋给了c,因为a和b都是八位的无符号字符
 //所以这个位或运算也是一个八位的无符号字符
 // 65的二进制:0100 0001
 // 66的二进制:0100 0010
 // 按位或运算:全为0才是0,最少有一个1,就为1
 // 故此可以得:0100 0011 = 67
 d = a + b;
 // 将a和b相加,赋值给d.这里发生了整数溢出,因为两个无符号字符相加
 // 如果结果超出了一个无符号字符的最大值(255)
 // 因此,d得到了一个负值
 printf("c = a | b = %d\n", c);
 printf("c = a | b = %c\n", c);
 printf("c = a | b = %x\n", c);
 printf("#################\n");
 printf("d = a + b = %d\n", d);
 printf("d = a + b = %c\n", d);
 printf("d = a + b = %x\n", d);
 return 0;
}

关于linux的权限设置的问题(这里也做一个补充)
图片

chmod的全称是 "change mode" ,用于更改文件或目录的权限。例如,上述775表示,一个三位数的权限模式,每一位分别代表所有者(User)、同组用户(Group)和其他用户(Other)的权限。example表示要修改权限的文件或目录名。

然而,每个数字又可以分为三种权限,读(r=4),写(w=2),执行(x=1)。将这些权限相加,就可以得到相应的数字。所有者(User)拥有读(4)、写(2)和执行(1)的权限,因此所有者的权限值为4+2+1=7。同组用户(Group)也拥有读(4)、写(2)和执行(1)的权限,因此同组用户的权限值也是4+2+1=7。其他用户(Other)只有读(4)和执行(1)的权限,没有写入权限,因此其他用户的权限值为4+1=5。

所以,chmod 775 example将会把名为example的文件或目录权限设置为:rwxrwxr-x。其中-表示没有相应权限。这意味着所有者和同组用户都有读、写和执行的权限,而其他用户则只有读和执行的权限。

素数筛算法是一种用于生成素数列表的高效算法。它通过逐步消除合数(非素数)来找到一个给定范围内的所有素数。

  • 1)标记一个范围内的数字是否是合数,没有被标记的则为素数;
  • 2)算法的空间复杂度为O(N),时间复杂度为O(N*loglogN);
  • 3)总体的思想是,用素数去标记掉不是素数的数字,例如,我知道了i是素数,那么2i、3i、4*i……就都不是素数。

如上只是素数筛算法的一个基本思想,现将实现素数筛算法的基本流程也写下来。

  • 1)用prime[i]来标记i是否是合数;看i是否被标记,如果被标记,那么i就是合数。
  • 2)标记为1的数字为合数,否则为素数;
  • 3)第一次知道2是素数,则将2的倍数标记为1;
  • 4)向后找到第一个没有被标记的数字i;
  • 5)将i的倍数全部标记为合数;
  • 6)重复4 - 6步骤,直到标记完范围内所有数字。
#include <stdio.h>
int prime[1000] = {0};//定义一个数组,求1000以内的所有素数
//并且把这个数组所有的数初始化为0
//定义一个函数,这里我们传入的参数是要初始化的范围n
//等于现在要求的是n以内的所有素数
void init_prime(int n){
 int i,j;
 prime[0] = prime[1] = 1; //将两个值标记为非素数(0和1)
 //for(i = 2; i <= n; i++)
 //从2开始依次遍历数组每一个数字
 /*
  * 当我们要标记到这个n范围以内的素数的时候,
  * 外层循环的过程只需要循环到根号n即可;
  * 因为我们是用数字i去标记所有i的倍数.
  * 假设一个数字n,如果是合数,那么一定可以拆成两个非1数字的乘积(n=a*b)
  * 那么a和b中,一定有一个值是<=根号n的
  * 这里可以使用反证法,如果a和b都大于根号n,那么a*b>n,不可能等于n
  * 如果n是合数,那么它当中一定存在一个因子是<=根号n的
  * 那么这个数字n它一定会被一个<=根号的素数给筛掉
 */
 for(i = 2; i*i <= n; i++)
 {
  if(prime[i]) continue;//如果prime[i]被标记(不等于0),说明i不是素数,就continue
  //for(j = 2*i; j <= n; j += i) //i是素数,将i所有的倍数全部标记(2*i)
  printf("%d is prime:", i); //将素数i打印出来
  for(j = i*i; j <= n; j += i)
  {
   //这个循环主要就是枚举所有i的倍数(2,3,4,5,6...)
   prime[j] = 1;
   printf(" %d", j);
  }
  printf("\n");
 }
 return ;
}
int main(int argc, const char *argv[]){
 init_prime(100);//先初始化100以内所有的素数的信息
 int x;
 while(~scanf("%d", &x))
 {
  printf("prime[%d] = %d\n", x, prime[x]);
 }
 return 0;
}
2.2 二分查找算法

在一个有序的数组中,想要查找一个数字x是否存在。二分查找算法(Binary Search Algorithm)是一种在有序数组中查找特定元素的搜索算法。它的基本思想是每次比较中间元素,将待查找的范围缩小为之前的一半,直到找到目标元素或确定目标元素不存在。不过在学习二分查找之前,这里我们依旧是再来学一个补充知识。

随机数的使用:
在C语言中,你可以使用rand()函数生成伪随机数。rand()函数会返回一个介于0(包括)和RAND_MAX(不包括)之间的整数值。但是,由于rand()产生的随机性并不强,你需要用srand()函数来设置随机数种子以获得更好的随机效果。

图片

二分查找的思想(难点)
假设现在我要到min和max之间去查找一个x,而现在不再是一个一个地去比较,而是先看一个中间的mid这个值,因为当前数组是一个有序数组,所以就用中间的值和x去比较,如果中间的值大于x,那么x只有可能存在在前半段(mid之前);如果中间的值小于x,那么x只有可能存在于后半段;而如果是等于x,那么就意味着找到了x这个值的位置。(效率高,单次迭代,范围则缩小一半)。

  • 1)计算中间索引:mid = (left + right) / 2;
  • 2)检查中间元素是否为目标元素:
    • 如果 arr[mid] == target,则返回 mid
    • 如果 arr[mid] < target,则在右半部分继续查找,即设置 left = mid + 1
    • 如果 arr[mid] > target,则在左半部分继续查找,即设置 right = mid - 1
  • 3)重复步骤 1 和 2,直到找到目标元素或 left > right(此时表明目标元素不在数组中)。

图片

解释:假设我们现在要查找一个7,因为中间值小于目标值7,所以我们把头指针移到中间值的下一个;假设要查找一个4,因为中间值大于目标值4,所以我们把尾指针移到中间值的上一个。

#include <stdlib.h>
#include <time.h>
int binary_search(int arr[], int l, int r, int target){
 while(l <= r)
 {
  int mid = (l + r) / 2;
  if(arr[mid] == target)
  {
   return mid;
  }
  else if(arr[mid] < target)
  {
   l = mid + 1;
  }
  else
  {
   r = mid - 1;
  }
 }
 return -1;
}
int main(int argc, const char *argv[]){
 int arr[] = {2, 4, 6, 8, 10, 12, 14, 16, 18, 20};
 int n = sizeof(arr)/sizeof(arr[0]);
 int target = 16;
 int result = binary_search(arr, 0, n-1, target);
 if(result != -1)
 {
  printf("目标元素%d在数组中的索引为%d\n", target, result);
 }
 else
 {
  printf("目标元素%d不在数组中\n", target);
 }
 return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int i;
int main() {
    srand(time(0));
    int arr[10] = {0};
    for (i = 1; i < 10; i++) {
        arr[i] = arr[i - 1] + (rand() % 10);
    }
    int len = 0;
    for (i = 0; i < 10; i++) {
        len += printf("%4d", i);
    }
    printf("\n");
    for (i = 0; i < len; i++) printf("-");
    printf("\n");
    for (i = 0; i < 10; i++) {
        printf("%4d", arr[i]);
    }
    printf("\n");
    int x, i;
    while (scanf("%d", &x) != EOF) {
        int cnt1 = 0, cnt2 = 0, flag1 = 0, flag2 = 0;
        for (i = 0; i < 10; i++) {
            cnt1 += 1;
            if (arr[i] != x) continue;
            flag1 = 1;
            break;
        }

        int l = 0, r = 9, mid;
        while (l <= r) {
            cnt2 += 1;
            mid = (l + r) >> 1;
            if (arr[mid] == x) {
                printf("(%d) arr[%d] = %d, find %d\n", cnt2, mid, arr[mid], x);
                flag2 = 1;
                break;
            }
            if (arr[mid] > x) {
                printf("(%d) arr[%d] = %d > %d, change [%d, %d] to [%d, %d]\n",
                      cnt2, mid, arr[mid], x, l, r, l, mid - 1
                );
                r = mid - 1;
            } else {
                printf("(%d) arr[%d] = %d < %d, change [%d, %d] to [%d, %d]\n",
                      cnt2, mid, arr[mid], x, l, r, mid + 1, r
                );
                l = mid + 1;
            }
        }
        printf("flag1 = %d, cnt1 = %d\n", flag1, cnt1);
        printf("flag2 = %d, cnt2 = %d\n", flag2, cnt2);
    }
    return 0;
}

图片

三、多维数组的定义与使用

多维数组在C语言中是通过嵌套的方括号[](也就是带多个方括号的)来定义的。这些数组允许你组织数据,使它们更容易表示和处理多维数据结构。二维数组是最常用的多维数组类型,但也可以创建更高维度的数组。

图片

#include <stdio.h>
int i, j;
int main(int argc, const char *argv[]){
 int b[3][4], cnt = 1;
 for(i = 0; i < 3; i++)
 {
  for(j = 0; j < 4; j++)
  {
   b[i][j] = cnt;
   cnt += 1;

   printf("%4d", b[i][j]);
  }
  printf("\n");
 }
 return 0;
}

图片

四、字符数组、字符串数组及操作

空终止字节字符串(Null-terminated byte string,NUL-terminated string)是一种表示文本字符串的常见方式,特别是在C语言及其相关编程语言中。这种类型的字符串由一个或多个字符组成,并以特定的空终止符(通常是一个ASCII值为0的字符)作为结尾。

在内存中,一个空终止字节字符串实际上是一个包含字符数据的数组,其中最后一个元素是空字符('\0')。例如,字符串"hello"在内存中的表示如下:

h e l l o \0

这种表示方式使得程序员可以很容易地通过遍历字符数组来确定字符串的长度:只需要找到第一个空字符即可停止。这也意味着字符串的最大长度不能超过可用内存空间减去1(用于存储空字符)。

在C语言中,空终止字节字符串是使用char *类型来表示的,这实际上是指向第一个字符的指针。为了方便处理这些字符串,C标准库提供了许多函数,如strlen()strcpy()strcat()strcmp()memcpy()等。

空终止字节字符串的一个主要优点是简单和高效,因为它们不需要额外的开销来存储字符串长度。然而,它们也有缺点,比如容易导致缓冲区溢出的安全问题,以及对非空终止字符串的支持不足。

// 定义字符数组
char str[size];
// 初始化字符数组
char str[] = "hello world";  //5+5+1+1=12(空格也算是一个),所以该数组大小是12
char str[size] = {'h','e','l','l','o'};
函数 说明
strlen(str) 计算字符串的长度,以  '\0'  作为结束符
strcmp(str1, str2); 字符串比较
strcpy(dest, src); 字符串拷贝
strncmp(str1, str2, n); 安全的字符串比较
strncpy(str1, str2, n); 安全的字符串拷贝
memcpy(str1, str2, n); 内存拷贝
memcmp(str1, str2, n); 内存比较
memset(str1, c, n); 内存设置
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
int main(int argc, const char *argv[]){
 char str1[10] = "abc";
 printf("str1 = %s\n", str1);
 // str1 = "def"; 错误赋值方式,要在定义的时候才能赋值
 strcpy(str1, "def");// 把"def"拷贝给str1[]这个数组
 printf("str1 = %s\n", str1);

 char str2[] = "hello\0 world";
 printf("strlen(str2) = %u\n", strlen(str2)); // strlen(str) = 5, 这只是统计字符串长度,到0就终止
 printf("sizeof(str2) = %u\n", sizeof(str2)); // strlen(str) = 13, 这是这个字符串的大小
 printf("str2 = %s\n", str2); // 输出str2的内容,是hello
 str2[5] = 'A';//把第五位,也就是\0改成字符A,注意要用单引号赋值给第五位
 printf("str2 = %s\n", str2);

 char str3[] = "abcdef", str4[] = "abc";
 printf("strcmp(str3, str4) = %d\n", strcmp(str3, str4));//输出十进制整数值
 printf("str3[3] = %d\n", str3[3]);//输出ASCII值
 //关于这个str3和str4的图解请看下图

 // memset方法常用场景:将一个数组中的值全部赋值为0
 int i;
 int arr[10];

 srand(time(0));
 for(i = 0; i < 10; i++)
 {
  arr[i] = rand() % 100;
 }
 for(i = 0; i < 10; i++)
 {
  printf("arr[%d] = %d\n", i, arr[i]);
 }
 // 将arr[10]数组全部赋值为0
 memset(arr, 0, sizeof(arr));
 // 第一个位置arr:要放的是初始化的内存的起始地址(即,数组的首地址);
 // 第二个位置0:因为把每个位置初始化为0,所以放置0值;
 // 第三个位置:放置要初始化的多少个字节,使用sizeof来算(或者直接写40,
 // 以及sizeof(int)*10
 for(i = 0; i < 10; i++)
 {
  printf("arr[%d] = %d\n", i, arr[i]);
 }
 //memset是对于一个范围之内的所有的字节去进行赋值
 return 0;
}

五、数组的存储方式

1)行序优先

俗称:“一行一行的存储”。
图片

2)列序优先

俗称:“一列一列的存储”。
图片

#include <stdio.h>
int main(int argc, const char *argv[]){
 int a[10][10];
 printf("&a[0][0] = %p\n", &a[0][0]);
 printf("&a[0][1] = %p\n", &a[0][1]);
 printf("&a[1][0] = %p\n", &a[1][0]);
 return 0;
}

图片

根据上面的输出数组a的地址的简单的例子,显而易见,C语言关于数组的存储方式就是以行序优先存储为主的。

六、初识指针

1、指针变量也是变量

如,整型变量存储整型值,浮点变量存储浮点值,字符变量存储字符数据……那么指针变量存储指针值,那么,指针值是什么呢?没错,它就是前面说到的地址。指针的变量指向一个地址,或者说,指针就是一个地址。故此,指针变量与其他变量的主要区别在于它们存储的内容不同:普通变量存储的是某种类型的值(如整数、字符或浮点数),而指针变量则存储的是其他变量或对象在内存中的地址。

指针变量也是变量。就像其他类型的变量一样,指针变量在程序中也有一个唯一的标识符(名字),并且在内存中占有一定的存储空间。

不同的是,普通变量存储的是具体的数值或者数据,而指针变量存储的是另一个变量或数据结构在内存中的地址。这意味着,通过操作指针变量,我们可以间接地访问和修改它所指向的内存位置的数据。

和其他变量一样,我们也可以对指针变量进行赋值、传递给函数、作为函数的返回值等操作。但是,由于指针直接涉及到内存操作,所以在使用时需要特别注意其安全性和有效性,避免出现指针错误,如空指针引用、野指针、内存泄漏等问题。

//定义一个存储整型地址的指针变量(不赋值)
int *ptr;
//定义一个存储双精度浮点型的指针变量(赋值)
double *ptr1;
double a = 88.88;
ptr1 = &a;
//输出指针指向的所存储的字符型的地址
char *ptr2;
char b = 'a';
ptr2 = &b;
printf("ptr2 = %p\n", ptr2);
printf("&b = %p\n", &b);
//输出指针指向的所存储的字符型的地址的值
printf("ptr2 = %c\n", *ptr2);
#include <stdio.h>
int main(int argc, const char *argv[]){
 int a = 10;
 //int *p = NULL;
 int *p = &a;
 printf("a = %p\na = %d\n", p, a);
 printf("sizeof(*p) = %d\n", sizeof(*p));
 printf("sizeof(a) = %d\n", sizeof(a));
 return 0;
}

图片

2、函数传递指针变量的场景和用途
#include <stdio.h>
int main(int argc, const char *argv[]){
 int *p1;
 double *p2;
 char *p3;
 int a = 123;
 double b = 88.88;
 char c = 'a';
 p1 = &a;
 p2 = &b;
 p3 = &c;
 printf("p1 = %p\n&a = %p\n", p1, &a);
 printf("\n");
 printf("p2 = %p\n&b = %p\n", p2, &b);
 printf("\n");
 printf("p3 = %p\n&c = %p\n", p3, &c);
    printf("\n\n");
 printf("*p1 = %d\n", *p1);
 printf("*p2 = %lf\n", *p2);
 printf("*p3 = %c\n", *p3);
 return 0;
}

图片

上面的代码也更加证实了指针变量存储的是某个变量的地址这一关系。如,p1 = &a,指针变量存储了一个整型变量的地址。后面以此类推……当我们使用取值运算符的时候,我们相当于是取到了指针中所存储地址里面所存储的值(*p1……)。

但是,如果在我们的函数里面传入一个指针类型的参数,那么它会具有那些用途呢?

2.1 用指针去定义传入参数

当我们想在函数里面修改实参值的时候,那我们就可以将相应的实参的地址传入到函数内部,也就是作为一个参数将实参的地址传进来。

图片
图片

2.2 用指针去定义传出参数

当我们在设计函数时,有某个参数的性质是传出参数的时候,也就是说,相应的位置,它不是用来传入值的,而是用来传出值的,是用来接收这个函数内部处理完以后的那个结果的。这个时候我们需要把它定义为指针变量。

#include <stdio.h>
//void add_once(int x)//传入一个整型x
void add_once(int *p)//传入一个整型的地址p
{
 *p += 1;//对整型指针取值,取到里面的值后再加1
 return ;
}
void f(int n, int *sum_addr)//传入一个整型n和一个sum的地址
{
 *sum_addr = (1 + n) * n / 2;
 return ;
}
int main(int argc, const char *argv[]){
 int a = 123;
 printf("a = %d\n", a);
 add_once(&a);//因为上面是一个指针变量,所以这里要传入一个地址
 printf("a = %d\n", a);//这里如果想对传入实参的值做改变,可以使用指针的技巧

 int n = 10, sum;
 f(n, &sum);
 //实现一个函数,传入一个n值,请将从1加到n的累加之和存储到sum中
 //因为是要修改实参的值,所以还是传实参的地址(&sum, 即,传出参数)
 //因为sum不是传入给f的一个参数,他是用来接收f函数返回值的参数
 //他的功能是用来往外传初值的,就是用来接收这个处理值的
 printf("sum = %d\n", sum);
 return 0;
}
2.3 用指针去接收数组参数

指针用来接收相应的数组。如果想接收整型数组,那么就要定义对应的函数的整型指针就行了。同理,如果要接收一个字符型的数组,那么则定义一个字符型的指针。

#include <stdio.h>
//void add_once(int x)//传入一个整型x
void add_once(int *p)//传入一个整型的地址p
{
 *p += 1;//对整型指针取值,取到里面的值后再加1
 return ;
}
void f(int n, int *sum_addr)//传入一个整型n和一个sum的地址
{
 *sum_addr = (1 + n) * n / 2;
 return ;
}
//数组名=整个数组中的第一个地址(即, arr = &arr[0])
void output(int *p, int n)//定义一个整型指针用来接收数组名(即p的地址)
{
 int i;
 for(i = 0; i < n; i++)
 {
  printf("p[%d] = %d\n", i, p[i]);//p[i]:[],方括号在C语言中不是特殊的语法,是一种运算符
  //而运算符就存在等价运算,如,2+2+2 <=> 2*3, 显然2+2+2比2*3更麻烦
  //那么p[i]的等价形式是什么呢?p[i] <=> *(p+i)
 }
 return ;
}
int main(int argc, const char *argv[]){
 int a = 123;
 printf("a = %d\n", a);
 add_once(&a);//因为上面是一个指针变量,所以这里要传入一个地址
 printf("a = %d\n", a);//这里如果想对传入实参的值做改变,可以使用指针的技巧

 int n = 10, sum;
 f(n, &sum);
 //实现一个函数,传入一个n值,请将从1加到n的累加之和存储到sum中
 //因为是要修改实参的值,所以还是传实参的地址(&sum, 即,传出参数)
 //因为sum不是传入给f的一个参数,他是用来接收f函数返回值的参数
 //他的功能是用来往外传初值的,就是用来接收这个处理值的
 printf("sum = %d\n", sum);

 int arr[10] = {1,3,5,7,9,2,4,6,8,0};
 //定义一个函数,输出arr数组前十位的值
 output(arr, 10);
 return 0;
}
2.4 交换指针变量(练习题)

图片

#include <stdio.h>
int swap(int *a, int *b){
 // *a ^= *b;
 // *b ^= *a;
 // *a ^= *b;
 int temp;
 temp = *a;
 *a = *b;
 *b = temp;//这种交换方法就不会有那种bug
}
int main(int argc, const char *argv[]){
 int a, b;
 scanf("%d%d", &a, &b);
 printf("a = %d\nb = %d\n", a, b);
 swap(&a, &b);//通过swap()方法去交换a和b变量中的值
 printf("a = %d\nb = %d\n", a, b);
 return 0;
}

图片

七、深入指针

1、地址的操作与取值规则
1.1 深入理解什么是“p+1”

这句话主要是在说,指针当中所存储的地址上,并不是说,这个指针变量加1,而是指针的地址中的值加1。比如。当int a+1;,这里也是变量中所存储的值加1,并不是说这个整型变量加1。那么这个指针加1(p+1)操作,其实是代表了加上了几呢?请看下面的图解。

图片

#include <stdio.h>
int main(int argc, const char *argv[]){
 int a, *p1 = &a;
 double b, *p2 = &b;
 char c, *p3 = &c;
 printf("p1 = %p\n", p1);
 printf("p1+1 = %p\n", p1+1);
 printf("p1+2 = %p\n", p1+2);
 printf("p1+3 = %p\n", p1+3);
 printf("p2 = %p\n", p2);
 printf("p2+1 = %p\n", p2+1);
 printf("p2+2 = %p\n", p2+2);
 printf("p2+3 = %p\n", p2+3);
 printf("p3 = %p\n", p3);
 printf("p3+1 = %p\n", p3+1);
 printf("p3+2 = %p\n", p3+2);
 printf("p3+3 = %p\n", p3+3);
 return 0;
}

上面代码也比较简单,无非就是定义指针变量。并且输出指针指向某个类型变量的地址,再输出这个地址的值,再输出这个地址加1的值,这里也就验证了指针加1就是加上了一个指针所指向变量的那个长度。

1.2 指针和数组的关系

为什么指针可以当成一个数组去用?指针和数组又有啥关系呢?下面我们同样通过一个程序来验证一下这个疑问。

#include <stdio.h>
int main(int argc, const char *argv[]){
 int arr[5] = {1, 2, 3, 4, 5};
 int *p;//定义一个指针,让这个指针存储arr数组的首地址
 p = &arr[0];
 int i;
 for(i = 0; i < 4; i++)
 {
  printf("p + %d = %p\n", i, p+i);//输出p3+i的地址
  printf("&arr[%d] = %p\n", i, &arr[i]);//输出数组第i个元素的地址
 }
 return 0;
}

图片

1.3 特殊语法

图片

#include <stdio.h>
int main(int argc, const char *argv[]){
 int arr[5] = {1, 2, 3, 4, 5};
 int *p1;//定义一个指针,让这个指针存储arr数组的首地址
 int (*p2)[10] = 0x0;//定义一个存储10个元素的整型数组类型的指针(并赋一个0值)
 //等于是这个指针的加1规则是一次性往后跳10*4=40个字节(就是跳过10个整型元素)
 p1 = &arr[0];
 int i;
 for(i = 0; i < 4; i++)
 {
  printf("p + %d = %p\n", i, p1+i);//输出p3+i的地址
  printf("&arr[%d] = %p\n", i, &arr[i]);//输出数组第i个元素的地址
 }
 printf("p2 = %p\n", p2);
 printf("p2 + 1 = %p\n", p2 + 1);
 printf("p2 + 2 = %p\n", p2 + 2);
 printf("p2 + 3 = %p\n", p2 + 3);
 return 0;
}

图片

1.4 int  (p2)[5]和int p2[5]的区别

上面我们已经熟知了int  (*p2)[5]的用法,它不是一个数组,而是一个指向10个整型元素的指针。那么我们去掉它的小括号,int *p2[5],那么现在p2就是一个数组,剩下的就是这个数组所存储的类型,所以这个数组中存储的每个元素的类型就是一个整型的地址,所以int *p2[5]相当于是定义了5个整型指针变量。

那么下面这个又是表达什么意思呢?

int *(*p3[10])[20];  //指针数组

看上面这个指针数组,我们首先要从变量名开始看,如果*号是跟着变量名的,那么这个变量就是一个指针。然后,变量名后面有方括号,说明他是一个数组。所以,根据这两句话,可以得出p3就是一个指针数组。后面方括号是[10],说明定义了10个指针(p0、p1、p2、……、p9)。那么这10个指针存储的是什么类型的地址呢?这10个指针存储的是int *[20]类型的地址(也就是存储20个元素的整型地址的这样一个数组)。 而p6这个数组中,它存储的每个位置都存储一个这种类型变量的地址。

图片

#include <stdio.h>
int main(int argc, const char *argv[]){
 int arr1[5] = {1, 2, 3, 4, 5};
 int *p1;//定义一个指针,让这个指针存储arr数组的首地址
 int (*p2)[10] = 0x0;//定义一个存储10个元素的整型数组类型的指针(并赋一个0值)
 //int (*p2)[10]:它其实可以指向一个数组,就是第二维为10的一个数组,如下:
 int arr2[30][10];//然后,arr2的地址赋值给p2,为什么呢?
 //当对p2+1时,相当于往后跳了10个整型;p2+1相当于是arr2[1]的首地址,p2+2相当于是arr2[2]的首地址
 //所以p2[1] <=> &arr2[1]
 p4 = arr2;
 //等于是这个指针的加1规则是一次性往后跳10*4=40个字节(就是跳过10个整型元素)
 p1 = &arr[0];
 int i;
 for(i = 0; i < 4; i++)
 {
  printf("p + %d = %p\n", i, p1+i);//输出p3+i的地址
  printf("&arr1[%d] = %p\n", i, &arr1[i]);//输出数组第i个元素的地址
 }
 printf("p2 = %p\n", p2);
 printf("p2 + 1 = %p\n", p2 + 1);
 printf("p2 + 2 = %p\n", p2 + 2);
 printf("p2 + 3 = %p\n", p2 + 3);
 return 0;
}
//上述代码定义有些问题,都写这么代码了,相信自己可以看出来,或者敲一遍,慢慢调试
2、深入理解*p操作

取值运算,那一定是取出来一部分数据,那么它取出来的是什么样的数据?从哪里取数据呢?带着这两个疑问,一起来探索下面的内容把!对于从哪里取数据,那肯定是从内存,熟悉系统底层的就知道,我们的计算机分为内存和外存,内存是断电丢失,外存是断电不丢失,因为内存会从外存读取数据来运行,我们的程序可以是写在外存里面,但是在内存运行的,所以就是从内存读取数据。那么它取几个字节的数据应是由什么所决定的呢?这个时候我们就要明白指针所指向的类型是什么?因为指针在取值的时候,就是取对应类型的数据。如果指针指向的是2字节的数据,所取出来的就是2字节的数据;如果指针指向的是8字节的数据,所取出来的就是8字节的数据。

那么指针在取值的时候,它究竟取出来几个自己的数据呢?这个和内存中存储什么样的数据有关系吗?没有关系。它只和指针所指向的类型有关系。

图片

根据上图所示,我们首要就是搞清楚指针的类型到底是什么?而对于内存中,究竟存储什么样的数据,这个并不重要。指针的类型才是最重要的,因为我们在取值的时候,是按照指针所指向的类型来取值的,而这个内存中究竟存储的是什么类型的数据,也只有我们自己在乎,指针不会在乎。

指针给我们提供了一种直接在内存的力度上去操作数据的途径。比如,在内存上原本存储的是一个4字节的整型,但是,如果拿一个字符型的指针去取值时,那么这时候取出来的会是什么值呢?这个情况取出来的也是一个字节的值。

也就是说,指针的取值,它取出来的值。只和指针所指向的类型有关,和内存中存储的数据类型无关。

总的来说,指针在取值时取出的数据量是由指针的类型和内存中实际存储的数据共同决定的。它并不取自己的数据(指针变量本身只存储一个内存地址),而是取它所指向的内存地址中的数据。这个数据可以是一个基本的数据类型,也可以是复杂类型如数组、结构体等的数据。指针类型决定了取几个字节,内存里存储的数据决定了你取到的值究竟是什么。

#include <stdio.h>
int main(int argc, const char *argv[]){
 int n = 0x61626364;//用一个十六进制去初始化整型占四个字节的数据
 //第一个字节中的数据是:0x61626364
 //char *p = &n;//定义一个字符型的指针p,让这个指针指向整型n的第一个地址
 //这里相当于是把整型的地址赋值给了一个字符类型的指针,所以需要用到强制类型转换
 //两边的类型是不匹配的
 char *p = (char *)&n;
 printf("*(p + 0) = %c\n", *(p + 0));//取第一个地址的值
 printf("*(p + 1) = %c\n", *(p + 1));//取第一个地址的值
 printf("*(p + 2) = %c\n", *(p + 2));//取第一个地址的值
 printf("*(p + 3) = %c\n", *(p + 3));//取第一个地址的值
 return 0;
}

图片

下面一起看到上图的运行结果,我们来分析一下。
图片

这里的访问数据的语法与之前的a[b] = *(a+b)有些类似。那么这里也是一样的情况,大家自行敲个代码测试一下吧!

3、指针的几种等价形式

首先,当我们单独写一个指针变量的时候,我们就知道这个单独的指针变量代表了某一个地址,这个算是我们的第一种表达形式。

1)p  <=>  &a

这就是把某个变量的地址等价为指针变量;

2)p + 1  <=>  1 + p

这就是关于指针运算的交换律;

3)p + 1  <=>  &p[1]

这里的p+1所对应的就是我们可以把p当成一个数组,这个数组的第二个就是下标为1的这个元素的地址;所以p+1等价于p1元素的地址,那么p+n也就等价于&p[n]这个元素的地址。

那么又为什么要列这种等价表达形式呢?因为当我们在理解一个指针表达式的时候,如果哪种表达方式对你来说更易于理解,那么你就可以转换成自己更易于理解的方式。

4)*p  <=>  p[0]  <=>  a

这里就是取值。当我们对于一个指针变量取值的时候,首先第一个它就等价于p[0];那它为什么等价于p[0]?p[0]代表的即是整个数组的第一个元素的值。同时我们可以把*p当成是某个变量或者某个类型的变量a。

5)p[n]  <=>  *(p+n)

这里说的就是方括号运算符的这个规则。这里的*(p+n)p+n就是要先跳到第n个元素的那个地址,加上星号就是将第n个元素的值给它取出来。

#include <stdio.h>
int main(int argc, const char *argv[]){
 int arr[] = {0, 1, 2, 3, 4, 5, 6};
 int *p = arr;
 int i;
 for (i = 0; i < 3; i++)
 {
  printf("%d\n", (i + 5)[&p[1] - 2]);
 }
 return 0;
}
6)分析:(i + 5)[&p[1] - 2]

根据上述等价形式可以得出,我们知道p + 1  <=>  &p[1],所以先可以得到(i + 5)[p + 1 - 2],又得,(i + 5)[p - 1];进而又得,*(i + 5 + p - 1),又得,(p+i+4)。然后我们可以把i+4看成一个整体,即可得到,p[i + 4]。根据上述程序,i的取值是从0到2的,故而这里i+4的取值就变成了4~6,所以访问的就是p[4]p[5]p[6]的值,这这里存储的分别就是456

八、特殊的指针

1、数组指针与函数指针

那么什么是数组指针?什么又是函数指针呢?数组指针(也称为“指向数组的指针”)是指向数组首元素的指针。它存储的是数组起始地址,通过解引用或偏移量可以访问数组中的各个元素。函数指针是用来指向函数地址的一种特殊指针类型,它允许我们像操作普通变量那样去调用一个函数。声明函数指针时,其类型需要与所指向的函数返回类型和参数列表完全匹配。总结来说,数组指针用来指向连续存储的多个数据元素集合,而函数指针则指向可执行代码块。

int arr1[10];
int arr2[10][10];
int *p1 = arr1;
int (*p2)[10] = arr2;  //这里一定要加小括号,不然p2就变成了一个单纯的数组了
//如果不加小括号,就表示这是一个存储10个int*类型的数组了,只有加上小括号之后,p2就不是数组名了,它是一个指针名。
//那么这个指针它所指向的类型是什么呢?它是指向一个拥有10个元素的整型的这样的一个存储空间
//那么为什么我们可以用p2这个指针去存储arr2数组的首地址呢?因为arr2+1和p2+1的规则是一样的,那么arr2+1等价于&arr2[1]
int (*add)(int int);//它可以指向所有的返回值为int传入两个整型参数的这种函数
#include <stdio.h>
void test1(){
 printf("function_test1\n");
 return ;
}
void test2(){
 printf("function_test2\n");
 return ;
}
void test3(){
 printf("function_test3\n");
 return ;
}
void (*p)();
int main(int argc, const char *argv[]){
 p = test1;
 p();
 p = test2;
 p();
 p = test3;
 p();
 return 0;
}

图片

那么现在我们来实现一个小小的功能,就是,我们希望主函数随机执行十次,然后随机调用test1到test3三个函数。在C语言中,如果要使用随机函数的话,我们需要包含两个头文件:

#include <stdlib.h>
#include <time.h>

然后我们要在主函数的开始初始化随机种子:

srand(time(0));

再然后我们把三个函数,也就是test1到test3存储到一个函数指针数组中。首先要先定义一个函数指针数组:

void arr(); //这是一个函数
void (*arr)(); //这是一个函数指针
//那么如何变成一个函数指针数组呢?
void (*arr[3])();//故此这个函数指针数组就有三位,分别是arr[0]、arr[1]、arr[2]
void (*arr[3])() = {test1, test2, test3};  //初始化这个函数指针数组
int i;
for(i = 0; i < 10; i++){
    arr[rand() % 3]();
    //每次随机调用一个函数,因为rand()%3所产生的值一定是0、1、2,用小括号来调用这个函数
}

图片

现在我们来区分一下几个概念:

1)指针数组的本质就是一个数组

是这个数组里的每一位存储的是个指针类型。

2)数组指针的本质就是一个指针

说白了就是这个它只是一个指针,它只占用一个指针的存储空间,它当中存储的是某种类型数组的地址。

3)数组指针数组

它的本质是一个数组。而这数组的每一个位置存储的是一个数组指针。

4)函数指针数组

它的本质是一个数组。而这数组的每一个位置存储的是一个函数指针。

九、内存管理方法

指针是内存地址的抽象,直接操作指针可以实现对内存中数据的精准定位和修改,但同时需要注意防止野指针和内存泄漏等问题。从内存的角度去控制程序的数据,这个就是指针的能力。程序在运行时需要使用内存来存储数据,可以通过动态内存分配函数(如C语言中的malloccallocreallocfreememsetmemcpymemmove)根据实际需求申请和释放内存空间。那么我们该如何去动态的申请被指针修改的这个内存空间呢?

函数 说明
malloc函数 动态申请一段内存空间
calloc函数 动态申请一段内存空间,初始化0值
free函数 释放动态申请的空间(当申请完动态空间不用的时候,请一定要还给系统空间,养成良好的编程习惯)
memset函数 设置每个字节为一个固定值
memcpy函数 内存数据拷贝(无重叠)
memmove函数 同memcpy,能处理空间有重叠的情况
#include <stdio.h>
#include <stdlib.h> //因为使用了malloc,包含一下头文件
//之后如果有头文件要包含请自己查询,命令是, man 3 malloc ...
int main(int argc, const char *argv[]){
 int *arr = (int *)malloc(sizeof(int) * 20);

 //malloc里要传入的是我们要申请的这个空间的字节数
 //注意:它传入的是字节数量
 //这里我们申请的是可以存储20个整形的存储空间
 //因为一个整形是4个字节,故此20个整形占80个字节
 //但是,直接在括号里写80,这种编码习惯显然会更不灵活
 //如果写成"sizeof(int)*10",那它更具有可移植性,故而保证这段代码在不同的系统中都可以运行
 //malloc函数的返回值类型是一个任意类型的指针类型,即,void *
 //在我们的指针类型中,void *是一个比较特殊的存在,意味着就是它存储的就是一个地址
 //但是我们可以把这个地址转换成任意类型的地址
 //然后malloc的返回值就是一个void *的地址,这个地址就代表了我们所申请存储空间的首地址
 //接下来我们就可以将malloc的返回值赋值给相应的指针了
 //然后我们将malloc的返回值转换成我们需要的这个指针类型
 //比如,上面我们转换的是整型的指针类型,(int *)
 //这个时候我们就可以将array当成一个拥有20个元素的数组来使用了
 int i;
 for (i = 0; i < 10; i++)
 {
  arr[i] = rand() % 100;
 }
 for (i = 0; i < 10; i++)
 {
  printf("arr[%d] = %d\n", i, arr[i]);
 }
 return 0;
}

图片

说明现在arr已经被我们当成一个数组在用。而它它所指向的这个空间就是动态申请内存的方式去申请出来的。而这个动态申请出来的内存就在操作系统的堆区中。对于操作系统的堆区和栈区的区别,可以自己百度。我在前面的文章已经提到过,也可当成是一个复习。

下面这段程序就是我们要测试上面的理论的情况,即,memcpy和memmove两个函数有重叠和没有重叠的一个实现过程和结果。

#include <stdio.h>
#include <stdlib.h> //因为使用了malloc,包含一下头文件
#include <string.h> //因为使用了memcpy和memmove这两个函数,需要包含头文件<string.h>
//之后如果有头文件要包含请自己查询,命令是, man 3 malloc ...
int main(int argc, const char *argv[]){
 int *arr1 = (int *)malloc(sizeof(int) * 10);

 //malloc里要传入的是我们要申请的这个空间的字节数
 //注意:它传入的是字节数量
 //这里我们申请的是可以存储20个整形的存储空间
 //因为一个整形是4个字节,故此20个整形占80个字节
 //但是,直接在括号里写80,这种编码习惯显然会更不灵活
 //如果写成"sizeof(int)*10",那它更具有可移植性,故而保证这段代码在不同的系统中都可以运行
 //malloc函数的返回值类型是一个任意类型的指针类型,即,void *
 //在我们的指针类型中,void *是一个比较特殊的存在,意味着就是它存储的就是一个地址
 //但是我们可以把这个地址转换成任意类型的地址
 //然后malloc的返回值就是一个void *的地址,这个地址就代表了我们所申请存储空间的首地址
 //接下来我们就可以将malloc的返回值赋值给相应的指针了
 //然后我们将malloc的返回值转换成我们需要的这个指针类型
 //比如,上面我们转换的是整型的指针类型,(int *)
 //这个时候我们就可以将array当成一个拥有20个元素的数组来使用了

 int *arr2 = (int *)calloc(10, sizeof(int));
 //这里表示开辟10个元素,每个元素的大小是一个整型这么大
 //calloc的返回值也是void *
 //它传入的参数有两项: void * calloc(size_t count, size_t size);
 //size_t count表示它要开辟几个元素;size_t size表示它每个元素的大小
 int i;
 for (i = 0; i < 10; i++)
 {
  printf("arr1[%d] = %d\n", i, arr1[i]);
 }
 for (i = 0; i < 10; i++)
 {
  printf("arr2[%d] = %d\n", i, arr2[i]);
 }

 free(arr1);
 free(arr2); //释放空间

 char s1[100] = "hello world"; //注意:这里实际上是11+1个字符,因为'\0'也算一个字符
 char s2[100];
 char s3[100];
 memcpy(s2, s1, 12);
 memmove(s3, s1, 12);
 printf("s2 = %s\n", s2);
 printf("s3 = %s\n", s3);

 memcpy(s2 + 4, s2, 12);
 memmove(s3 + 4, s3, 12);
 printf("s2 + 4 = %s\n", s2);
 printf("s3 + 4 = %s\n", s3);
 return 0;
}

图片
图片

十、结尾:typedef关键字

那么什么又是typedef关键字呢?typedef关键字是C++(以及C)中用于创建类型别名的关键字。它允许我们为已存在的数据类型定义一个新的名称,这样我们就可以在程序中使用这个新的、更简洁或更具描述性的名字来代替原本复杂的类型声明。通过使用typedef关键字,代码可读性得到提升,并且可以减少类型声明的复杂性,特别是在处理复杂模板或者嵌套指针类型时尤其有用。(总之一句话就是,把某类型的变量名变成某类型的别名)。

内建类型的重命名:

typedef long long LL;
typedef char * pchar;

结构体类型的重命名:

typedef struct __node{
    int x, y;
} Node, *PNode;

函数指针命名:

typedef int (*func)(int);

实例:

#include <stdio.h>
typedef long long LL;
typedef int (*Arr2Dim10)[10];
typedef void (*Func)();

void test(){
    printf("hello function pointer!\n");
    return ;
}
int main(){
    LL a;
    printf("sizeof(a) = %lu\n", sizeof(a));

    int arr[5][10];
    Arr2Dim10 p = arr;

    Func p2 = test;
    p2();

    return 0;
}

END




上一篇:AI能力体系三层结构解析:如何区分OpenSkills、AgentSkills与Anthropic Skills
下一篇:AI赋能Web3开发:从需求到技术架构的Next.js实战设计
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-24 17:08 , Processed in 0.160088 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表