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语言提高(一)_第1张图片

函数封装和数组形参退化为指针

因为数组做形参会退化为指针,所以,如果在被调函数中涉及到数组的长度的时候,只能把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语言提高(一)_第2张图片
写大于0的值都没关系,因为都是把它当作传入数组的首地址来操作的。所以干脆直接写成指针。

数据类型本质

数据类型的本质:固定内存块大小的别名

#include<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第3张图片

变量的本质

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

内存分区模型

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

全局区

以文字常量区为例分析全局区

#include<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第4张图片

栈区

与上面的有区别的,上面是指针,这里是数组。

#include<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第5张图片
运行结果:
在这里插入图片描述
从这里看是仍保留着之前的内容。但是,如果是在一个很大的程序中,就有可能存在着程序崩溃的隐患。或者在不同的编译器中,输出的结果有可能是乱码。

这个例子还不太好说明问题,因为有可能strcpy之后,get_str才释放,这样就避免了我们想要凸显的问题了。

我们对这个程序进行改进下。

#include<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第6张图片

堆区

#include<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第7张图片
画图分析:
C语言提高(一)_第8张图片

函数的调用模型

C语言提高(一)_第9张图片

函数调用变量传递分析

1、fun()和fun2()都在main函数中
C语言提高(一)_第10张图片
2、
C语言提高(一)_第11张图片
3、fun2(a)嵌套在fun1()中。
C语言提高(一)_第12张图片
4、
C语言提高(一)_第13张图片
5、
C语言提高(一)_第14张图片

静态局部变量的使用

静态局部变量放在全局区。

栈地址的生长方向

栈地址的生长方向是向下(递减)的。
C语言提高(一)_第15张图片

堆地址的生长方向

堆地址的生长方向是向上(递增)的。

#include<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第16张图片
画图示意:
C语言提高(一)_第17张图片

指针强化

强化一

指针也是数据类型

指针也是数据类型,指针变量也是一种变量。
C语言提高(一)_第18张图片
画图示意怎么画。
C语言提高(一)_第19张图片

通过*来操作内存

#include<stdio.h>
#include<stdlib.h>
#include<string.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<stdio.h>
#include<stdlib.h>
#include<string.h>

int main(void)	
{
	//写内存时,一定要确保内存可写
	char buf[] = "abcd";

	buf[2] = 'a';

	printf("buf = %s",buf);

	printf("\n");
	system("pause");
	return 0;
}

字符串从字符常量区复制到了buf数组中。所以修改不会出错。
C语言提高(一)_第20张图片
直接操作内存常量区时,会报错。

不允许向NULL和未知非法地址拷贝内存

C语言提高(一)_第21张图片
C语言提高(一)_第22张图片

强化二

理解指针必须和内存四区相结合

怎样定义一个变量来保存自身的地址?

答:在原来类型基础上加一个*

int main(void)	
{
	//一个变量,应该定义一个怎样类型的指针保存它的地址
	//在原来类型基础上加一个*
	int	 a = 10;
	int*  p = &a;
	int** q = &p;
	
	int******* t = NULL;
	int******** u = &t;

	printf("\n");
	system("pause");
	return 0;
}

主调函数和被调函数

主调函数可把堆区、栈区、全局数据内存地址传给被调用函数。
被调函数只能返回堆区和全局数据。
(前面 “函数调用变量传递分析” 也提到过)

内存分配方式

指针做函数参数,是有输入和输出特性的
C语言提高(一)_第23张图片

输入特性

#include<stdio.h>
#include<stdlib.h>
#include<string.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<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第24张图片

字符串初始化

#include<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第25张图片

数组法和指针法操作字符串

C语言提高(一)_第26张图片

自己写字符串拷贝函数

C语言提高(一)_第27张图片
简化:
C语言提高(一)_第28张图片
简化
C语言提高(一)_第29张图片
最终的简洁写法:
C语言提高(一)_第30张图片

完善字符串拷贝函数

C语言提高(一)_第31张图片
必须的:判断形参指针是否为NULL。
不是必须的:(改变首地址时使用(比如(str++)),移动时不使用(比如(str[begin],bigen变)))最好不要直接使用形参,而是要添加辅助变量,这样可以在调试或打印输出时保证指针指向实参的首地址。

项目开发常用字符串应用模型

strstr中的while和do-while模型

利用strstr标准库函数找出一个字符串中substr出现的个数。

do-while模型

#include<stdio.h>
#include<stdlib.h>
#include<string.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<stdio.h>
#include<stdlib.h>
#include<string.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<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <ctype.h>

/* 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<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <ctype.h>

/* 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<stdio.h>
#include<stdlib.h>
#include<string.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<stdio.h>
#include<stdlib.h>
#include<string.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<stdio.h>
#include<stdlib.h>
#include<string.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;
}
#endif

typedef 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修饰的变量

在这里插入图片描述
在const.c中
C语言提高(一)_第32张图片
在另一个.c中
C语言提高(一)_第33张图片
运行结果:
在这里插入图片描述

C语言中const是一个冒牌货

为什么说它是冒牌的呢?
是因为虽然我们不能直接修改被const修饰的变量的值,但是我们可以通过间接的方式进行修改。

#include<stdio.h>
#include<stdlib.h>
#include<string.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;
}

运行结果如下:
在这里插入图片描述

二级指针

如果 一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量。也称为二级指针。

二级指针做函数参数—输出特性

#include<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第34张图片
注意:
1、地址传递,形参修改会影响到实参,所以要定义一个临时变量。
2、输出特性,在被调函数中分配空间。使用malloc后,在被调函数结束后要释放。

二级指针做函数参数—输入特性

第一种输入模型

指针数组的定义与使用

#include<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第35张图片
改进代码,变的灵活一些。
特别注意:这种改进是有条件的,需要在定义指针数组时不指定长度。
也就是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<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第36张图片

对上述代码进行函数封装

这里面有一个过渡非常重要。
C语言提高(一)_第37张图片
因此,

#include<stdio.h>
#include<stdlib.h>
#include<string.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<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第38张图片
求二维数组中一维数组的个数(行数):
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个字节。

验证如下:
在这里插入图片描述
加4个字节
在这里插入图片描述
加4个字节
在这里插入图片描述
加5个字节

这里还有特别重要的一点,在指针数组中交换的是指针的指向。
而在这里,我们只能交换内存块。
交换的是内存块,而不是指针的指向。
因为这里的a[i],*(a+i)本身都是固定的地址值(都是常量),常量是不能修改的。
所以要使用strcpy来交换内存块。

代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第39张图片
这样空间分配好以后,问题又来了。
我们能否直接进行字符串拷贝操作:

	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语言提高(一)_第40张图片
最后,还要释放堆内存,先释放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<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第41张图片

封装函数改进-多级指针的使用

参考输出特性。增加临时指针变量

#include<stdio.h>
#include<stdlib.h>
#include<string.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语言提高(一)_第42张图片
释放分析:
C语言提高(一)_第43张图片

你可能感兴趣的:(c语言)