C语言学习第009课——指针的应用

值传递和地址传递

值传递

extern void swap(int a,int b);
int main(){
     
	int a = 10;
	int b = 20;
	swap(a,b);
	printf("%d\n",a);			结果为10
	printf("%d\n",b);			结果为20
	return 0;
}
void swap(int a,int b){
     
	int temp = a;
	a = b;
	b = temp;
}

以上代码中,将a b直接传递给函数swap,进行值对调的操作之后,并没有改变最终的a和b的值,
这是因为在C语言中,值传递这种方式,形参不影响实参的值
原理是:

1、在内存中分配a的内存空间
2、在内存中分配b的内存空间
3、在内存中加载swap函数
4、在内存中分配形参a,b,temp
5、将步骤4中分配的a和b值进行对调
6、swap函数结束,方法出栈,内存中将步骤3 4 5的操作痕迹进行销毁
7、打印a b的值依然为10  20

地址传递

extern void swap(int* a,int* b);
int main(){
     
	int a = 10;
	int b = 20;
	swap(&a,&b);
	printf("%d\n",a);			结果为20
	printf("%d\n",b);			结果为10
	return 0;
}
void swap(int* a,int* b){
     
	int temp = *a;
	*a = *b;
	*b = temp;
}

以上代码中,将a b的地址作为参数传递给函数swap,进行值对调的操作之后,最终改变了a和b的值
这是因为地址传递的方式,形参可以改变实参的值
原理是:

1、在内存中分配a的内存空间
2、在内存中分配b的内存空间
3、在内存中加载swap函数
4、在内存中分配形参指针a,指针b,int类型变量temp
5、将指针a对应的值10,赋值给temp
6、将指针b对应的值20,赋值给指针a对应的地址,这里并没有对指针a进行什么操作,而是通过指针a间接的改变步骤1中a的值
7、将temp的值10,赋值给指针b对应的地址,改变的也是步骤2中b的值
8、swap函数结束,方法出栈,内存中销毁步骤34567的数据
9、此时步骤1 2中的值已经发生了改变 a = 20, b = 10

同样的道理,之前学的冒泡排序函数,字符串复制函数,进行的都是地址传递,所以能够在方法出栈之后,依然能改变变量的值

数组名做函数参数

数组名做函数的参数,函数的形参(数组名)会退化为指针
例子:给一个字符串ch1后面追加另一个字符串ch2:
方法一:数组思路:

extern void my_strcat(char* ch1,char* ch2);
int main(){
     
	char ch1[100] = "hello";
	char ch2[] = "world";
	my_stradd(ch1,ch2);	     将ch2的内容添加到ch1后面
	printf("%s\n",ch1);
	return 0;
}
								
void my_strcat(char* ch1,char* ch2){
     	
	int i = 0;
	while(ch1[i] != '\0'){
     
		i++;
	 }
	int j = 0;
	while(ch2[j] != '\0'){
     
		ch1[i+j] = ch2[j];
		 j++;
	}
}

问题:之前说数组名作为函数参数的时候要连同数组长度也一起传进来,为啥他就不用传递数组长度进来呢?
解答:因为字符数组比较特殊,就像代码中ch1分配的长度是100,但是我们有更精确的方式来判断字符串的长度,就是判断’\0’,再一个,此处如果将100传进来, 确实起不到什么作用
问题:第二个while循环之后,还没有给字符串末尾加’\0’,是不是应该加一行:

ch1[i+j] = '\0';

解答:理论上应该加,但是这里可以不用写,因为main函数中ch1的长度定义了100,这么多位置除了hello之外,其他的位置全是0,所以这一行已经帮我们做了,我们可以不用写
方法二:指针思路

void my_stradd(char* ch1,char* ch2){
     
	int i = 0;
	while(*(ch1 + i) != '\0'){
     
		i++;
	}
	int j = 0;
	while(*(ch2 + j) != '\0'){
     
		*(ch1 + i + j) = *(ch2 + j);
			j++;
	}
}
就是将方法一中的数组名+下标 变成 指针+偏移量

方法三:利用指针自增代替指针偏移量

void my_stradd(char* ch1,char* ch2){
     
	while( *ch1 )ch1++;
	while(*ch2){
     
		*ch1 = *ch2;
		ch1++;
		ch2++;
	}
}

方法四:精简指针自增赋值

void my_stradd(char* ch1,char* ch2){
     
	while( *ch1 )ch1++;
	while(*ch1++ = *ch2++);
}

第二行代码中,包含的操作有:

1*ch1 = *ch2;
2、ch1++;  ch2++;    这里涉及到运算符优先级的问题
3while(*ch1 != 0)

例子:字符串去空格
方法一:数组思路:遍历原字符数组,遇到不是空格的,将他放进一个新的字符数组中,最后拼成一个新的字符数组

extern void str_remove_space(char* ch);
int main(){
     
	char ch[] = "h   e      ll   o w   o r  l d";
	str_remove_space(ch);
	return 0;
}
void str_remove_space(char* ch){
     
	char str[100] = {
     0};		定义一个字符数组,用来存放过滤后的字符串
	int i = 0,j = 0;
	while(ch[i] != '\0'){
     
		if(ch[i] != ' '){
     
			str[j] = ch[i];			给新数组赋值
			j++;
		}
		i++;
	}
	printf("%s\n",str);
}

第一个方法中,使用指针思路也一样,无论怎么改原理都差不多,但是这里面使用到了一个新创建的数组,这种办法并不好,万一我传入的非空格字符多于100就完了
所以可不可以不用创建这样一个新的字符数组呢?
方法二:
思路:定义指向字符数组的两个指针,一个指针一直往前遍历数组,另一个指针指向数组起始位置,第一个指针遍历过程中,遇到不是空格的字符,就将第二个指针, 指向的位置替换成这个字符,指针++

void str_remove_space(char* ch){
     
	char* i = ch;			这里将ch本身作为第一个指针,将i作为第二个指针
	while(*ch != '\0'){
     
		if(*ch != ' '){
     
			*i = *ch;
			i++;
		}
		ch++;
	}
	*i = 0;			将第二个指针最后一位写0
}

指针作为函数的返回值

例子:字符串中查找一个字符,并且返回该字符所在的指针

extern char* my_strchr(char* str,char ch);
int main(){
     
    char* str = "hello world";
    char* p = my_strchr(str,'r');
    if(p == NULL){
     
        printf("未找到\n");
    }else{
     
        printf("%s\n",p);
    }
    return 0;
}

char* my_strchr(char* str,char ch){
     
    while(*str){
     
        if(*str == ch){
     
            return str;
        }
        str++;
    }
    return NULL;
}

例子:字符串中查找另一个字符串,并且返回该字符串所在的指针
分析:
首先需要一个指针1,用来遍历源字符串
再定义一个指针2,用来遍历目标字符串
还需要定义一个指针3,用来指向源字符串中和目标字符串相匹配的第一个位置,用来全部匹配上之后返回的
思路:
遍历源字符串,如果和目标字符串的第一个字符相等,则保存指针3,开始比较源字符串和目标字符串之后的内容,最后匹配上了,返回指针3即可

extern char* my_strstr(char* src,char* dest);
int main(){
     
    char src[] = "hello world";
    char dest[] = "wo";
    char* p = my_strstr(src,dest);
    if(p == NULL){
     
        printf("null\n");
    }else{
     
        printf("%s\n",p);
    }
    return 0;
}

char* my_strstr(char* src,char* dest){
     
    char* fsrc = src;       						遍历src的指针
    char* fdest = dest;     						目标字符串指针
    char* rsrc = src;       						待返回指针
    while(*fsrc){
     									判断是否读到了字符串末尾
        rsrc = fsrc;								待返回指针先跟着遍历指针走
        while(*fsrc == *fdest && *fsrc != '\0'){
     	判断当遍历指针的值和目标指针的值一样的话,待返回指针就不动了,while开始比较当前
        											指针指向的值和目标指针指向的值,如果一样,指针自加,继续比对,如果不一样,跳出循
        											环,*fsrc != '\0'是防止目标字符串刚好和源字符串的末尾相同,导致数组越界,影响
        											结果
            fsrc++;
            fdest++;
        }
        if(*fdest == '\0'){
     							跳出循环之后,判断目标指针是不是指向字符串末尾,用来判断目标字符串是不是完全匹配
            return rsrc;
        }
        //如果没匹配上,回滚
        fsrc = rsrc;								如果没有匹配上,源字符串指针要指到一开始和目标字符串开始比较的地方
        fdest = dest;								目标指针也要指向目标字符串开始的地方
        fsrc++;										源字符串自增,开始下一轮查找
    }
    return NULL;
}

指针和字符串

例子:定义一个字符串

char ch[] = "hello world";
char *p = ch;

考虑:既然如上代码是成立的,那么可以不可以这样定义一个字符串呢?

char* p = "hello world";	将指针直接指向字符串

这样是可以,不会报错,而且打印和获取单个字符都可以执行
但是还是有区别的地方的,例如,修改字符串中的某个字符

char ch[] = "hello world";
ch[2] = 'm';
printf("%s\n",ch);			打印结果:hemlo world

char *p = "hello world";
*(p+2) = 'm';
printf("%s\n",*p);			会报错

报错原因在于:

char ch[] = "hello world";		这种定义方式,定义的是栈区字符串
char* p = "hello world";		这种定义方式,定义在数据区常量区字符串

栈区定义的字符串可以修改,常量区字符串不允许修改,
例如最开始的printf(“hello world”);中的字符串就是存储在数据区常量区的
另外一个例子可以直观的理解数据区常量区字符串的概念:

char ch[] = "hello world";
char *p = "hello world";
char *p1 = "hello world";
printf("%p\n",ch);			0028FF2C
printf("%p\n",p);			00405064
printf("%p\n",p1);			00405064

可以看出,通过指针定义的字符串打印的地址相同,说明指针定义字符串是常量,可以改变指针p的值,但是不能改变指针指向地址的值(*p)
指针指向的区域是常量,只读的,所以多个指针指向同一个字符串地址是一样的

字符串数组

定义字符数组
方式一:

char ch1[] = "hello";
char ch2[] = "world";
char ch3[] = "dabaobei";
char* arr[] = {
     ch1,ch2,ch3}; 

这种方式定义的字符串数组是可以改变字符串的值的,根据上面的例子,定义不能改变字符串的值的方法为:

char* arr[] = {
     "hello","world","dabaobei"};

遍历字符串数组中的元素:

for(int i = 0;i<3;i++)
{
     
    printf("%s\n",arr[i]);
}

但是怎么样给arr中的元素排序呢?
如果定义方式是方式一的话,是不能进行排序的,因为前两个字符串长度一样,可以进行交换,第三个字符串和前两个字符串长度不一样,无法进行调换,那下面的方式二有什么办法呢?

char* arr[] = {
     "hello","world","dabaobei"};

分析:方式二的字符串是存放在数据区常量区的,无法对字符串本身进行改变,但是每个字符串有对应的内存地址:如0x10 0x20 0x30分别存放hello world dabaobei
而在另一区域存储着指针变量arr,arr[0]的值为0x10,arr[1]的值为0x20,arr[2]的值为0x30
而排序必定会用到冒泡排序这些方法,肯定会涉及到两个首字母进行比较,然后两个进行位置调换
试想如果hello和world两个字符串需要调换位置,那么只需要将arr[0]的值和arr[1]的值进行调换就可以了,此时arr[0] arr[1] arr[2]对应的值分别为:
0x20 0x10 0x30
遍历arr中的值,第一个出来的就是0x20地址所对应的字符串,可以实现排序

char* arr[] = {
     "hello","world","dabaobei"};
for(int i = 0;i<3;i++)
{
     
   for(int j = 0;j<3-1-i;j++)
   {
     
       if(arr[j][0]>arr[j+1][0])    找首字符进行比较
       {
     
           char* temp = arr[j];
           arr[j] = arr[j+1];
           arr[j+1] = temp;
       }
   }
}
for(int k = 0;k<3;k++)
{
     
   printf("%s\n",arr[k]);   这里arr[k]也是指针类型,为啥打印的时候前面不用*取值符号呢?因为前面占位符写的是%s,这样的占位符会根据
   						    arr[k]所在的指定位置一直往后寻址取值打印,知道找到\0停止
}

运行结果为 hello world dabaobei

字符指针作为函数参数

计算字符串的有效长度
数组思路:

int my_strlen(char* ch)
{
     
    int i = 0;
    while(ch[i]!='\0')
    {
     
        i++;
    }
    return i;
}

指针思路

int my_strlen(char* ch)
{
     
    char* temp = ch;		定义一个辅助指针,和ch指向同一位置
    while(*(temp))temp++;	只要*temp不是0,temp一直自增
    return temp-ch;			最后temp的位置指向字符串结尾的\0,ch指向第一个字符,两者间的差值就是字符串的长度
}

一般情况下,像现在这样,我们想要获取到一个字符串的信息,例如长度信息,是不需要改变字符串的内容的,为了安全起见,可以使用const修饰函数的参数
需要注意的是,我们使用const修饰的目的是为了不让字符串的值发生改变,所以这里const应该修饰的是char*

int my_strlen(const char* ch)

const修饰的指针变量

第一种方式:

char ch1[] = "hello";
char ch2[] = "world";
const char* p = ch1;  这种情况下,const修饰的是char*,所以不可以修改p指向的字符串的内容,可以修改p指向的位置

也就是说:

*p = 'm';    err  这一行代码是想将h改成m
p[2] = 'm';  err  这个原因和上一行是一样的
p = ch2;     ok
这种使用const修饰指针类型(char*)的方式,我们称之为指向常量的指针

第二种方式:

char ch1[] = "hello";
char ch2[] = "world";
char* const p = ch1;    这种情况下,const修饰的是变量p,所以不可以修改p的指向,而可以修改*p也就是字符串的内容
*p = 'm';	ok
p = ch2;    err
这种使用const修饰指针变量(p)的方式.我们称之为指针常量	

第三种方式:

char ch1[] = "hello";
char ch2[] = "world";
const char* const p = ch1;  这种情况下,const修饰char*也修饰指针变量p,既不能改变p的指向,也不能改变字符串的内容
*p = 'm';	err
p = ch2;	err
这种既使用const修饰指针类型又使用const修饰指针变量的方式,我们称之为只读指针

相同的道理,一级指针的常量,可以使用二级指针进行修改,是可以的
例如,想要将hello变成hmllo,使用二级指针修改

char ch1[] = "hello";
const char* const p = ch1;
char** p1 = &p;			定义一个二级指针,指向一级指针p的地址
*(*p1+1) = 'm';			p1是二级指针,*p1就是一级指针,一级指针就很ch1一样,所以*p1加上偏移量,扩住再取值就是字符串的值

二级指针常量可以用三级指针进行修改,以此类推,所以说const修饰指针这样的方式是怎么样都不安全的,
但是在指针作为函数参数的时候,也只能加一个const约束一下

你可能感兴趣的:(C语言基础,c语言)