C语言提高(一)


C语言提高

  • CS和BS的区别
  • 函数封装和数组形参退化为指针
  • 数据类型本质
  • 变量的本质
  • 内存分区模型
    • 全局区
      • 以文字常量区为例分析全局区
    • 栈区
    • 堆区
  • 函数的调用模型
  • 函数调用变量传递分析
  • 静态局部变量的使用
  • 栈地址的生长方向
  • 堆地址的生长方向
  • 内存的存放方向-以数组为例
  • 指针强化
    • 强化一
      • 指针也是数据类型
      • 通过*来操作内存
      • 易错点
        • 写内存时,一定要确保内存可写
        • 不允许向NULL和未知非法地址拷贝内存
    • 强化二
      • 怎样定义一个变量来保存自身的地址?
      • 主调函数和被调函数
      • 内存分配方式
        • 输入特性
        • 输出特性
  • 字符串初始化
  • 数组法和指针法操作字符串
  • 自己写字符串拷贝函数
  • 完善字符串拷贝函数
  • 项目开发常用字符串应用模型
    • strstr中的while和do-while模型
      • do-while模型
      • while模型封装成函数
    • 两头堵模型
      • 求两头堵模型下非空字符串的个数
      • 求两头堵模型下非空字符串并输出
  • const的使用
    • 修饰一个变量为只读
      • 修饰普通变量为只读
      • 修饰普通指针变量为只读
      • 修饰结构体变量为只读
      • 如何引用另一个c文件中使用const修饰的变量
      • C语言中const是一个冒牌货
  • 二级指针
    • 二级指针做函数参数---输出特性
    • 二级指针做函数参数---输入特性
      • 第一种输入模型
        • 指针数组的定义与使用
        • 对指针数组中的成员进行排序
        • 对上述代码进行函数封装
      • 第二种内存模型
        • 封装成函数
      • 第三种内存模型
        • 导入
        • 封装函数
        • 封装函数改进-多级指针的使用

学习网址: https://www.bilibili.com/video/BV1Rt411m78c?p=8.
CS和BS的区别 C语言提高(一)
文章图片

函数封装和数组形参退化为指针 因为数组做形参会退化为指针,所以,如果在被调函数中涉及到数组的长度的时候,只能把n作为参数传进去(只能在主函数中求出),而在被调函数中求不出来。
//如果数组作为函数参数,数组形参退化为指针。 //void print_array(int a[10], int n) //void print_array(int a[1], int n) //void print_array(int a[], int n) //[]中的值写不写都行. void print_array(int *a, int n) { //a,当作指针用,指针类型,32位,长度是4个字节 //n = 4 / 4 = 1 n = sizeof(a) / sizeof(a[0]); //求数组中元素的个数 printf("print_array:n = %d.\n", n); int i; for (i = 0; i < n; i++) { printf("%2d ", a[i]); } printf("\n"); }

调用它
print_array(a, n); //传数组名,就是数组的首地址。

不能填0
C语言提高(一)
文章图片

写大于0的值都没关系,因为都是把它当作传入数组的首地址来操作的。所以干脆直接写成指针。
数据类型本质 数据类型的本质:固定内存块大小的别名
#include.h> #include.h> #include.h>int main(void) { int a[10]; //告诉编译器,分配40个字节 int b; //告诉编译器,分配4个字节 /* 类型的本质:固定内存块大小的别名 */ printf("sizeof(a) = %d, sizeof(b) = %d \n", sizeof(a), sizeof(b)); /* 打印地址 */ // 数组名字,数组首元素地址,数组首地址 printf("a:%d,&a:%d \n",a,&a); //a和&a的数组类型不一样 //a是数组首元素地址,一个元素是4字节,+1相当于加4个字节 //&a是整个数组的首地址,一个数组4*10 = 40字节,+1相当于加40个字节 printf("a+1:%d,&a+1:%d \n", a+1, &a+1); //指针类型的长度,32位程序,长度是4 //64位程序,长度是8 char****************** p = NULL; int* q = NULL; printf("%d,%d\n",sizeof(p),sizeof(q)); system("pause"); return 0; }

C语言提高(一)
文章图片

变量的本质 变量本质:一段连续内存空间的别名
变量相当于门牌号,内存相当于房间

内存分区模型 C++程序在执行时,将内存大方向划分为4个区域
? 代码区:存放函数体的二进制代码,由操作系统进行管理的
? 全局区:存放全局变量和静态变量以及常量
? 栈区:由编译器自动分配释放, 存放函数的参数值,局部变量等
? 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收

全局区 以文字常量区为例分析全局区
#include.h> #include.h> #include.h>char* get_str1() {// p 是栈区 char* p = "abcdefg"; //"abcdefg\0"放在文字常量区(全局区) return p; }char* get_str2() { char* q = "abcdefg"; //文字常量区(全局区) return q; } int main(void) { char* p = NULL; char* q = NULL; p = get_str1(); q = get_str2(); //%s:指针指向内存区域的内容 //%d:打印p本身的值 printf("p = %s,p = %d\n",p,p); printf("q = %s,q = %d\n",q,q); system("pause"); return 0; }

搞错的点:
//%s:指针指向内存区域的内容
//%d:打印p本身的值
printf(“p = %s,p = %d\n”,p,p);

运行结果:p、q指向同一块内存。
C语言提高(一)
文章图片

画图分析:
C语言提高(一)
文章图片

栈区 与上面的有区别的,上面是指针,这里是数组。
#include.h> #include.h> #include.h>char* get_str() { char str[] = "abcdefg"; return str; }int main(void) { char buf[128] = { 0 }; strcpy(buf, get_str()); //%s:指针指向内存区域的内容 //%d:打印p本身的值 printf("buf = %s.\n",buf); system("pause"); return 0; }

"abcdefg\0"在全局区。
画图分析:
C语言提高(一)
文章图片

运行结果:
C语言提高(一)
文章图片

从这里看是仍保留着之前的内容。但是,如果是在一个很大的程序中,就有可能存在着程序崩溃的隐患。或者在不同的编译器中,输出的结果有可能是乱码。
这个例子还不太好说明问题,因为有可能strcpy之后,get_str才释放,这样就避免了我们想要凸显的问题了。
我们对这个程序进行改进下。
#include.h> #include.h> #include.h>char* get_str() { char str[] = "abcdefg"; return str; }int main(void) { char* p = NULL; p = get_str(); //%s:指针指向内存区域的内容 printf("p = %s.\n", p); system("pause"); return 0; }

运行结果:
C语言提高(一)
文章图片

画图分析:
C语言提高(一)
文章图片

堆区
#include.h> #include.h> #include.h>char* get_str() { char *tmp = (char*)malloc(100); if (tmp == NULL) { return NULL; } strcpy(tmp, "abcdefg"); return tmp; }int main(void) { char* p = NULL; p = get_str(); if (p != NULL) { //%s:指针指向内存区域的内容 printf("p = %s.\n", p); free(p); // 解除p与堆区的指向关系,告诉操作系统可以让别人来操作这块区域了。堆区内容仍然存在。 p = NULL; //虽然解除指向关系之后,但p默认还是会指向堆区的。让p指向NULL,彻底脱离关系。 } printf("\n"); system("pause"); return 0; }

运行结果:
C语言提高(一)
文章图片

画图分析:
C语言提高(一)
文章图片

函数的调用模型 C语言提高(一)
文章图片

函数调用变量传递分析 1、fun()和fun2()都在main函数中
C语言提高(一)
文章图片

2、
C语言提高(一)
文章图片

3、fun2(a)嵌套在fun1()中。
C语言提高(一)
文章图片

4、
C语言提高(一)
文章图片

5、
C语言提高(一)
文章图片

静态局部变量的使用 静态局部变量放在全局区。
栈地址的生长方向 栈地址的生长方向是向下(递减)的。
C语言提高(一)
文章图片

堆地址的生长方向 堆地址的生长方向是向上(递增)的。
#include.h> #include.h> #include.h>int main(void) { int* c = (int*)malloc(4); int* d = (int*)malloc(4); printf("c = %d,d = %d\n", c, d); printf("\n"); system("pause"); return 0; }

C语言提高(一)
文章图片

内存的存放方向-以数组为例 给数组分配一块内存空间。
C语言提高(一)
文章图片

画图示意:
C语言提高(一)
文章图片

指针强化 强化一 指针也是数据类型
指针也是数据类型,指针变量也是一种变量。
C语言提高(一)
文章图片

画图示意怎么画。
C语言提高(一)
文章图片

通过*来操作内存
#include.h> #include.h> #include.h>int main(void) { int a = 100; int* p1 = NULL; //指针指向谁,就把谁的地址赋值给指针 p1 = &a; //*钥匙,通过*可以找到指针指向的内存区域,操作的是内存 *p1 = 22; // *放在“=”左边,给内存赋值,写内存 // *放在“=”右边,取内存的值,读内存 int b = *p1; printf(" b = %d\n",b); printf("\n"); system("pause"); return 0; }

易错点
写内存时,一定要确保内存可写
#include.h> #include.h> #include.h>int main(void) { //写内存时,一定要确保内存可写 char buf[] = "abcd"; buf[2] = 'a'; printf("buf = %s",buf); printf("\n"); system("pause"); return 0; }

字符串从字符常量区复制到了buf数组中。所以修改不会出错。
C语言提高(一)
文章图片

直接操作内存常量区时,会报错。
不允许向NULL和未知非法地址拷贝内存 C语言提高(一)
文章图片

C语言提高(一)
文章图片

强化二 理解指针必须和内存四区相结合
怎样定义一个变量来保存自身的地址?
答:在原来类型基础上加一个*
int main(void) { //一个变量,应该定义一个怎样类型的指针保存它的地址 //在原来类型基础上加一个* inta = 10; int*p = &a; int** q = &p; int******* t = NULL; int******** u = &t; printf("\n"); system("pause"); return 0; }

主调函数和被调函数
主调函数可把堆区、栈区、全局数据内存地址传给被调用函数。
被调函数只能返回堆区和全局数据。
(前面 “函数调用变量传递分析” 也提到过)
内存分配方式
指针做函数参数,是有输入和输出特性的
C语言提高(一)
文章图片

输入特性
#include.h> #include.h> #include.h>/* 指针做函数参数 -- 输入特性 *///参数列表中写 /* in */ ,表示有指向的内存区域, //且这个内存区域在主调函数中分配。 //这里所指向的内存区域就是buf void fun(char* p /* in */)//指针做函数参数 { //给p所指向的内存区域拷贝 strcpy(p,"abcdefg"); }void fun2(char* p /* in */)//指针做函数参数 { if (p == NULL) { return; } //给p所指向的内存区域拷贝 strcpy(p, "abcdefg"); }int main(void) { //输入,主调函数分配内存 char buf[100] = {0}; //分配好空间了 fun(buf); //把首地址传过去 printf("buf = %s\n", buf); char* str = NULL; fun2(str); printf("\n"); system("pause"); return 0; }

输出特性
#include.h> #include.h> #include.h>/* 指针做函数参数 -- 输出特性 */ void fun(char** p /* out */, int*len) { //先判断是否为空 if (p == NULL) { return; } char* tmp = (char*)malloc(100); if (tmp == NULL)//说明动态数组开辟失败 { return; } strcpy(tmp,"abcdefg"); //间接赋值 *p = tmp; *len = strlen(tmp); }int main(void) { //输出,被调用函数分配内存,必须地址传递,否则不能影响实参 char* p = NULL; int len = 0; fun(&p,&len); if (p != NULL) { printf("p=%s,len=%d.\n",p,len); } printf("\n"); system("pause"); return 0; }

画图分析,很重要。
C语言提高(一)
文章图片

字符串初始化
#include.h> #include.h> #include.h>/* C语言没有字符串类型,通过字符数据模拟 C语言字符串,以字符'\0'、数字0结尾。 */int main(void) { //不指定长度,没有'\0'结束符,有多少个元素就有多长 char buf[] = { 'a','b','c'}; printf("buf = %s\n",buf); //指定长度,后面没有赋值的元素,自动补0 char buf1[100] = { 'a','b','c' }; printf("buf1 = %s\n", buf1); char buf2[100] = { 'a','b','c','0','7' }; printf("buf2 = %s\n", buf2); char buf3[100] = { 'a','b','c',0 ,'7' }; printf("buf3 = %s\n", buf3); char buf4[100] = { 'a','b','c','\0' ,'7' }; printf("buf4 = %s\n", buf4); //使用字符串初始化,常用 char buf5[] = "abc"; printf("buf5 = %s\n", buf5); //strlen:测字符串长度,不包含数字0或字符'\0', //sizeof:测数组长度,包括数字0或字符'\0'。 printf("strlen(buf5) = %d ,sizeof(buf5) = %d\n", strlen(buf5), sizeof(buf5)); char buf6[100] = "abc"; //strlen:测字符串长度,不包含数字0或字符'\0', //sizeof:测数组长度,包括数字0或字符'\0'。 printf("strlen(buf6) = %d ,sizeof(buf6) = %d\n", strlen(buf6), sizeof(buf6)); printf("\n"); system("pause"); return 0; }

输出结果:
C语言提高(一)
文章图片

数组法和指针法操作字符串 C语言提高(一)
文章图片

自己写字符串拷贝函数 C语言提高(一)
文章图片

简化:
C语言提高(一)
文章图片

简化
C语言提高(一)
文章图片

最终的简洁写法:
C语言提高(一)
文章图片

完善字符串拷贝函数 C语言提高(一)
文章图片

必须的:判断形参指针是否为NULL。
不是必须的:(改变首地址时使用(比如(str++)),移动时不使用(比如(str[begin],bigen变)))最好不要直接使用形参,而是要添加辅助变量,这样可以在调试或打印输出时保证指针指向实参的首地址。
项目开发常用字符串应用模型 strstr中的while和do-while模型 利用strstr标准库函数找出一个字符串中substr出现的个数。
do-while模型
#include.h> #include.h> #include.h>int main(void) { char* p = "abcdefgabcdegfabcdefgh"; int n = 0; do { p = strstr(p,"abcd"); if (p != NULL) { n++; p += strlen("abcd"); } else //如果没有匹配的字符串,跳出循环 { break; } } while (*p != '\0'); printf("n = %d.\n", n); printf("\n"); system("pause"); return 0; }

while模型封装成函数
#include.h> #include.h> #include.h>int my_strstr(char* str, char* substr,int* n) { //辅助变量 int i = 0; char* tmp_str = str; char* tmp_substr = substr; //strstr函数返回匹配到的字符串的首地址 while ((tmp_str = strstr(tmp_str, tmp_substr)) != NULL) { //能进来,肯定有匹配的子串 i++; //重新设置起点位置 tmp_str += strlen(tmp_substr); if (*tmp_str == 0) { break; } } //间接赋值 *n = i; return 0; }int main(void) { char* str = "sdabcdefgabcdegfabcd"; char* substr = "abcd"; int n = 0; int ret = 0; ret = my_strstr(str, substr ,&n); //通过形参改实参的值,要使用地址传递。 if (!ret) { printf("n = %d.\n", n); } printf("\n"); system("pause"); return 0; }

两头堵模型 求两头堵模型下非空字符串的个数
#include.h> #include.h> #include.h> #include /* isspace(测试字符是否为空格字符) * 头文件 #include * 函数int isspqce(int c) * return:若参数c为空格就返回TRUE,否则返回NULL(0)。 */int my_isspace(char* str, int* n) { if(str == NULL || n ==NULL) { return -1; } int begin = 0; int end = strlen(str ) - 1; //记得要减一 // 从左开始 // 如果当前字符为空且没有结束 //或者直接判断(str [begin] =='') while (isspace(str [begin]) && str [begin] != 0) { begin++; //往右移动 } while (isspace(str [end]) && str [end] != 0) { end--; //往左移动 } *n = end - begin + 1; return 0; }int main(void) { // 两头堵,两头为空格 char* str = "cdegfabcd"; int n = 0; int ret = 0; ret = my_isspace(str, &n); if (!ret) { printf("n = %d\n", n); } printf("\n"); system("pause"); return 0; }

求两头堵模型下非空字符串并输出
#include.h> #include.h> #include.h> #include /* isspace(测试字符是否为空格字符) * 头文件 #include * 函数int isspqce(int c) * return:若参数c为空格就返回TRUE,否则返回NULL(0)。 */int my_isspace(char* str, char* buf) { if (str == NULL || buf == NULL) { return -1; } int begin = 0; int end = strlen(str) - 1; //记得要减一 int n = 0; // 从左开始 // 如果当前字符为空且没有结束 //或者直接判断(str[begin] =='') while (isspace(str[begin]) && str[begin] != 0) { begin++; //往右移动 } while (isspace(str[end]) && str[end] != 0) { end--; //往左移动 } n = end - begin + 1; strcpy(buf, str+begin,n); buf[n] = '\0'; return 0; }int main(void) { // 两头堵,两头为空格 char* str = "cdegfabcd"; char buf[100] = {0}; int n = 0; int ret = 0; ret = my_isspace(str, buf); if (!ret) { printf("buf = %s\n", buf); } printf("\n"); system("pause"); return 0; }

const的使用 修饰一个变量为只读 const修饰的变量,定义时需要初始化。
修饰普通变量为只读
#include.h> #include.h> #include.h>int main(void) { //const :修饰一个变量为只读 const int a = 10; a = 10; //err,a由const修饰,不能修改。 }

修饰普通指针变量为只读
const char *p = buf;
char *const p1 = buf;
const char *const p2 = buf;
#include.h> #include.h> #include.h>int main(void) { //指针变量和指针指向的内存(中的内容)是两个概念 char buf[] = "abcdefg"; char buf_b[] = "abcdefg_b"; //从左往右看,跳过数据类型,看修饰哪个字符 //如果是*,说明指针指向的内存(中的内容)不能改变 //如果是指针变量,说明指针的指向不能改变,指针的值不能改变 //跳过数据类型char,const修饰的是*,所以指针指向的内存(中的内容不能改变) const char* p = buf; //与 char const* p = buf; 等价 //也就是说*(p+i)='f' 或者 p[i] ='f'; 是错误的。 //但是指针的指向是可以变的,或者说指针的值可以改变。 //比如: p = buf_b; //同样的, char* const p1 = buf; //p1 = buf_b; //错误 p1[2] = 'f'; //都不能变 const char* const p2 = buf; printf("\n"); system("pause"); return 0; }

修饰结构体变量为只读
#include.h> #include.h> #include.h>#if 0 int main(void) { //指针变量和指针指向的内存(中的内容)是两个概念 char buf[] = "abcdefg"; char buf_b[] = "abcdefg_b"; //从左往右看,跳过数据类型,看修饰哪个字符 //如果是*,说明指针指向的内存(中的内容)不能改变 //如果是指针变量,说明指针的指向不能改变,指针的值不能改变 //跳过数据类型char,const修饰的是*,所以指针指向的内存(中的内容不能改变) const char* p = buf; //与 char const* p = buf; 等价 //也就是说*(p+i)='f' 或者 p[i] ='f'; 是错误的。 //但是指针的指向是可以变的,或者说指针的值可以改变。 //比如: p = buf_b; //同样的, char* const p1 = buf; //p1 = buf_b; //错误 p1[2] = 'f'; //都不能变 const char* const p2 = buf; printf("\n"); system("pause"); return 0; } #endiftypedef struct Mystruct { int a; int b; }Mystruct; void fun1(Mystruct * p) { //指针能变 p = NULL; //指针指向的内存(中的内容)也可以变 p->a = 10; }void fun2(Mystruct const *p) { //指针能变 p = NULL; //指针指向的内存(中的内容)不可以变 //p->a = 10; err }void fun3( Mystruct * const p) { //指针不能变 //p = NULL; err //指针指向的内存(中的内容)可以变 p->a = 10; }//p只读 void fun4(const Mystruct * const p) { //指针不能变 //p = NULL; //指针指向的内存(中的内容)也不可以变 //p->a = 10; Mystruct tmp; tmp.a = p->a; }int main(void) { printf("\n"); system("pause"); return 0; }

如何引用另一个c文件中使用const修饰的变量
C语言提高(一)
文章图片

在const.c中
C语言提高(一)
文章图片

在另一个.c中
C语言提高(一)
文章图片

运行结果:
C语言提高(一)
文章图片

C语言中const是一个冒牌货
为什么说它是冒牌的呢?
是因为虽然我们不能直接修改被const修饰的变量的值,但是我们可以通过间接的方式进行修改。
#include.h> #include.h> #include.h>int main(void) { const int b = 10; //b = 100; //erro int* q = &b; *q = 100; printf("b = %d",b); printf("\n"); system("pause"); return 0; }

运行结果如下:
C语言提高(一)
文章图片

二级指针 如果 一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。也称为二级指针。
二级指针做函数参数—输出特性
#include.h> #include.h> #include.h>int getmem(char** p) { char *tmp = (char*)malloc(sizeof(char) * 100); if (tmp == NULL) { return -1; } strcpy(tmp,"abcdefg"); *p = tmp; return 0; }int main(void) { char* p = NULL; int ret = 0; ret = getmem(&p); if (ret == 0) { printf("p = %s", p); } if (p != NULL) { free(p); p = NULL; } printf("\n"); system("pause"); return 0; }

画图分析:
C语言提高(一)
文章图片

注意:
1、地址传递,形参修改会影响到实参,所以要定义一个临时变量。
2、输出特性,在被调函数中分配空间。使用malloc后,在被调函数结束后要释放。

二级指针做函数参数—输入特性 第一种输入模型
指针数组的定义与使用
#include.h> #include.h> #include.h>int main(void) { //每个类型都是char* char *p0 = "0000"; char *p1 = "1111"; char *p2 = "2222"; char *p3 = "3333"; //换成用指针数组表示。两种表示,本质一样 //指针数组,指针的数组,他是一个数组,每一个元素都是指针char * char* p[] = { "0000" ,"1111" ,"2222" ,"3333" }; for (int i = 0; i < 4; i++) { printf("%s\n",p[i]); } printf("\n"); system("pause"); return 0; }

画图说明:
C语言提高(一)
文章图片

改进代码,变的灵活一些。
特别注意:这种改进是有条件的,需要在定义指针数组时不指定长度。
也就是char* p[]
而char* p[10]这样指定长度不行。

//改进代码,变的灵活一些 int n = sizeof(p) / sizeof(p[0]); for (int i = 0; i < n; i++) { printf("%s\n",p[i]); }

对指针数组中的成员进行排序
#include.h> #include.h> #include.h>int main(void) { //指针数组,指针的数组,他是一个数组,每一个元素都是指针char * char* p[] = { "aabb","0000" ,"cccc","1111" ,"2222","4444" ,"3333" }; int n = sizeof(p) / sizeof(p[0]); printf("数组中元素的个数:%d\n",n); // 7 int i = 0; int j = 0; char* tmp = NULL; printf("排序前:\n"); for ( i = 0; i <= n-1; i++) { printf("%s,",p[i]); } //比较排序,按每个字符串中的字符的大小来升序排列。 for (i = 0; i < n - 1; i++) { for (j = i + 1; j < n; j++) { //strcmp是字符串比较函数。 //strcmp(a,b) //字符a>b时,输出>0,相等时,输出0,小于时,输出<0 if (strcmp(p[i], p[j]) > 0) { tmp = p[i]; p[i] = p[j]; p[j] = tmp; } } } printf("\n排序后:\n"); for (i = 0; i <= n - 1; i++) { printf("%s,", p[i]); } printf("\n"); system("pause"); return 0; }

需要特别注意的一个地方。
C语言提高(一)
文章图片

对上述代码进行函数封装 这里面有一个过渡非常重要。
C语言提高(一)
文章图片

因此,
#include.h> #include.h> #include.h>// 对于形参,实参是什么类型,形参写跟它一样 //void printf_array(char* p[], int n) void printf_array(char** p, int n) { int i = 0; for (i = 0; i <= n - 1; i++) { printf("%s,", p[i]); } printf("\n"); }void sort_array(char** p, int n) { int i = 0; int j = 0; char* tmp = NULL; //比较排序,按每个字符串中的字符的大小来升序排列。 for (i = 0; i < n - 1; i++) { for (j = i + 1; j < n; j++) { //strcmp是字符串比较函数。 //函数原型:int strcmp(const char *s1,const char *s2); //参数:s1、s2是指向字符串的指针 //返回值: 自左向右逐个按照ASCII码值进行比较,直到出现不同的字符或遇’\0’为止。 //如果返回值 < 0,则表示 s1 小于 s2。 //如果返回值 > 0,则表示 s1 大于 s2。 //如果返回值 = 0,则表示 s1 等于 s2。if (strcmp(p[i], p[j]) > 0) { tmp = p[i]; p[i] = p[j]; p[j] = tmp; } } } }int main(void) { //指针数组,指针的数组,他是一个数组,每一个元素都是指针char * char* p[] = { "aabb","0000" ,"cccc","1111" ,"2222","4444" ,"3333" }; int n = sizeof(p) / sizeof(p[0]); printf("排序前:\n"); printf_array(p,n); sort_array(p, n); printf("排序后:\n"); printf_array(p, n); printf("\n"); system("pause"); return 0; }

第二种内存模型
#include.h> #include.h> #include.h>int main(void) { // char a0[5] = "aabb"; char a1[5] = "0000"; char a2[5] = "cccc"; char a3[5] = "1111"; //写法本质一样 //二维数组a[4][5]可以看作是4个a[5]的一维数组, char a[4][5] = { "aabb" ,"0000" ,"cccc" ,"1111" }; //a[0] = "aabb"; int i = 0; for (i = 0; i < 4; i++) { //下面三个输出结果一样 printf("%s\n", a + i); printf("%s\n", a[i]); printf("%s\n",*(a+i)); } printf("\n"); system("pause"); return 0; }

概念辨析和画图分析:
C语言提高(一)
文章图片

求二维数组中一维数组的个数(行数):
int n = sizeof(a) / sizeof(a[0]);

封装成函数 前面我们分析了,对于二维数组名a应该理解成是首行地址,而且a+1应该加一行,如果该行(一维数组)的长度为n,那么a+1应该加n*sizeof(数据类型)个字节。比如是char类型,n为5,那么就应该加5。
我们在定义被调函数的参数类型的时候,最最简单的方式就是主调函数的参数类型和被调参数类型定义一致。
主调函数的变量变量定义为为:
char a[][5] = { "aabb" ,"0000" ,"cccc" ,"1111" };

被调函数的参数定义:
void print_array(char a[][5], int n)

被调函数参数定义绝对不能是下面两种:
void print_array(char **a, int n) void print_array(char* a[], int n)

为什么呢?
实际上,这两种写法本质上是一回事。都是指针数组。前面也提到了指针数组每一个元素都是指针char *,加1相当于加4个字节。
而我们这里加1应该加n个字节。所有不可以这样定义形参。

我们可以验证下。是不是这样。
这里被调函数中的实参为:
char a[][5] = { "aabb" ,"0000" ,"cccc" ,"1111" };

a+1相当于加5个字节。
验证如下:
C语言提高(一)
文章图片

加4个字节
C语言提高(一)
文章图片

加4个字节
C语言提高(一)
文章图片

加5个字节
这里还有特别重要的一点,在指针数组中交换的是指针的指向。
而在这里,我们只能交换内存块。
交换的是内存块,而不是指针的指向。
因为这里的a[i],*(a+i)本身都是固定的地址值(都是常量),常量是不能修改的。
所以要使用strcpy来交换内存块。
代码如下:
#include.h> #include.h> #include.h>void print_array(char a[][5], int n) { int i = 0; for (i = 0; i < n; i++) { //下面三个输出结果一样 //printf("%s, ", a + i); printf("%s, ", a[i]); // printf("%s, ",*(a+i)); } printf("\n"); }void soft_array(char a[][5], int n) { int i ,j; char tmp[5]; for (i = 0; i < n - 1; i++) { for (j = i + 1; j < n; j++) { if (strcmp(a[i], a[j]) > 0) { //交换的是内存块,而非指针的指向 strcpy(tmp, a[i]); strcpy(a[i], a[j]); strcpy(a[j], tmp); } } } }int main(void) { //二维数组a[4][5]可以看作是4个a[5]的一维数组, char a[][5] = { "aabb" ,"0000" ,"cccc" ,"1111" }; int n = sizeof(a) / sizeof(a[0]); printf("排序前:\n"); print_array(a, n); soft_array(a, n); printf("排序后:\n"); print_array(a, n); printf("\n"); system("pause"); return 0; }

第三种内存模型
导入 静态分配一个空间,并拷贝字符串。
char p0_sta[100] = { 0 }; strcpy(p0_sta, "abcd"); printf("%s\n", p0_sta);

动态分配一个空间,并拷贝字符串,现在不释放。
char* p0_dyn = NULL; p0_dyn = (char *)malloc(100); strcpy(p0_dyn,"abcd"); printf("%s\n", p0_dyn);

动态分配十个上述空间,并拷贝字符串,现在不再释放。
//10个char *,每个值都是空(NULL) int i = 0; char* p[3] = {0}; for (i = 0; i < 3; i++) { p[i] = (char*)malloc(100); strcpy(p[i], "abcd"); }

能不能换一种方式(不用for循环),来分配上述空间呢?
再举个例子,
(其实这里可以不用举int的例子,直接用上面的char的例子,但是使用int可以突出开辟空间时所占内存大小的问题)
静态分配a[10]空间大小的内存
int a[10];

动态分配一个等价于a[10]空间大小的内存
int* q = (int *)malloc(10*sizeof(int)); if (q == NULL) { return -1; }

现在通过类比来模仿上面分配一个等价于char* p[3]空间大小的内存,
int n = 3; char** buf = (char **)malloc(n * sizeof(char*)); if (buf == NULL) { return -1; }

这样分配好之后,就相当于定义了char *p[3]。
画图来分析一下:
C语言提高(一)
文章图片

这样空间分配好以后,问题又来了。
我们能否直接进行字符串拷贝操作:
for (i = 0; i < n; i++) { strcpy(buf[i], "abcd"); }

答案是不可以的。我们完成这一步只是相当于
char* buf[3] = {0};

也就是说,p[i] = NULL,而写内存时是不允许向NULL和未知非法地址拷贝内存
因此,还需要在堆区开辟空间,需要注意类型。

for (i = 0; i < n; i++) { buf[i] = (char*)malloc(100); strcpy(buf[i], "abcd"); }

画图来说明:
C语言提高(一)
文章图片

最后,还要释放堆内存,先释放p[i]所指向的堆内存,然后再释放buf所指向的对内存。
//先释放buf[i]所指向的堆内存 for (i = 0; i < n; i++) { free(buf[i]); buf[i] = NULL; } //再释放buf所指向的堆内存 if (buf != NULL) { free(buf); buf = NULL; }

封装函数
#include.h> #include.h> #include.h>char** getMem(int n) { int i; char** buf = (char**)malloc(n * sizeof(char*)); if (buf == NULL) { return NULL; } for (i = 0; i < n; i++) { buf[i] = (char*)malloc(100); strcpy(buf[i], "abcdefg"); } return buf; } void print_array(char** buf,int n) { int i; for (i = 0; i < n; i++) { printf("%s\n", buf[i]); } }void freeMem(char** buf, int n) { int i; //先释放buf[i]所指向的堆内存 for (i = 0; i < n; i++) { free(buf[i]); buf[i] = NULL; } //再释放buf所指向的堆内存 if (buf != NULL) { free(buf); buf = NULL; } }int main(void) { int i; char** buf = NULL; int n = 3; buf = getMem(n); print_array(buf,n); //解除堆内的指向关系 freeMem(buf, n); //值传递,形参不影响实参 buf = NULL; printf("\n"); system("pause"); return 0; }

画图分析:
C语言提高(一)
文章图片

封装函数改进-多级指针的使用 参考输出特性。增加临时指针变量
#include.h> #include.h> #include.h>int getMem(char*** tmp,int n) { int i; char** buf = (char**)malloc(n * sizeof(char*)); if (buf == NULL) { return NULL; } for (i = 0; i < n; i++) { buf[i] = (char*)malloc(100); strcpy(buf[i], "abcdefg"); } *tmp = buf; return 0; } void print_array(char** buf,int n) { int i; for (i = 0; i < n; i++) { printf("%s\n", buf[i]); } }int freeMem(char*** tmp, int n) { int i; char** buf = *tmp; //先释放buf[i]所指向的堆内存 for (i = 0; i < n; i++) { free(buf[i]); buf[i] = NULL; } //再释放buf所指向的堆内存 if (buf != NULL) { free(buf); buf = NULL; } *tmp = NULL; return 0; }int main(void) { int i; char** buf = NULL; int n = 3; getMem(&buf,n); print_array(buf,n); //解除堆内的指向关系 freeMem(&buf, n); printf("\n"); system("pause"); return 0; }

【C语言提高(一)】分配分析:
C语言提高(一)
文章图片

释放分析:
C语言提高(一)
文章图片

    推荐阅读