IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> C++知识库 -> C语言再学习笔记 Part.1 -> 正文阅读

[C++知识库]C语言再学习笔记 Part.1

1. C语言中的NULL到底是什么?又不是什么?

在《C和指针》一书中有如下描述:

标准定义了 NULL指针,它作一个特殊的指针变量,表示不指向任何东西。要使一个指针变量为可给一个零值。为了测试一个指针变量是否为NULL,可以将与零值进行比较。之所以选择零这个值,是因为一种源代码约定。就机器内部而言,NULL指的际值可能此不同。在这种情况下,编译器将负责零值和内部值之间的翻译转换。

NULL可表特定的指针目前并未指向任何东西。例如,一个用于在某个数组中查找某个特定值的函数可能返回一个指向查找到的数组元素的指针。如果该数组不包含指定条件的值,函数就返回一个NULL指针。这个技巧允许返回值传达两个不同片段的信息。首先,有没有找到元素?其次,如果找到,它是哪个元素?

提示:
尽管这个技巧在C程序中极为常用,但它违背了软件工程的原则。用单一的值表示两种不同的意思是件危险的事,因为将来很容易无法弄清哪个才是它真正的用意。在大型的程序中,这个问题更为严重,因为你不可能在头脑中对整个设计一览无余。一种更为安全的策略是让函数返回两个独立的值:首先是个状态值,用于提示查找是否成功;其次是个指针,当状态值提示查找成功时,它所指向的就是查找到的元素。

对指针进行解引用操作可以获得它所指向的值。但从定义上看,NULL指针并未指向任何东西。因此,对一个NULL指针进行解引用操作是非法的。在对指针进行解引用操作之前,首先必须确保它并非 NULL 指针。

警告:
如果对一个 NULL指针进行间接访问会发生什么情况呢?它的结果因编译器而异。在有些机器上,它会访问内存位置零。编译器能够确保内存位置零没有存储任何变量,但机器并未妨碍你访问或修改这个位置。这种行为是非常不幸的,因为程序包含了一个错误,但机器却隐匿了它的症状,这样就使这个错误更加难以寻找。

在其他机器上,对 NULL指针进行间接访问将引发一个错误,并终止程序。宣布这个错误比隐藏这个错误要好得多,因为程序员能够更容易修正它。

提示:
如果所有的指针变量(而不仅仅是位于静态内存中的指针变量)能够被自动初始化为 NULL,那实在是件幸事,但事实并非如此。不论你的机器对解引用NULL指针这种行为作何反应,对所的指针变量进行显式的初始化是种好做法。如果已经知道指针将被初始化为什么地址,就把它初化为该地址,否则就把它初始化为LL。风格良好的程序会在指针解引用之前对它进行检查,这种初始化策略可以节省大量的调试时间。

一个C语言的笔试题:

#include <stdio.h>
 
void fun(int *node)
{
    static int N=100;
    node=&N;
}
int main()
{
    int *node=NULL;
    int a=0;
    fun(node);
    a=*node;
    printf("%d\n",a);
    
    return 0;
 
}

请回答,以上这题的输出结果?
100?0?段错误退出?哪一句导致的?为什么?

认为是第一个结果人其实是被static这个关键词欺骗的,但是static是对N的修饰,表示对N的改变不会在fun函数的‘}’之后被释放掉,还有一个点就是:C语言的函数永远是值传递(除了数组),所以你想改变指针的指向(地址值),就必须传递指针的指针,除非用return。

认为是第二个结果的人掌握了第一个结果的点,并且知道在C语言里是那样定义NULL的:

#undef NULL
#if defined(__cplusplus)
#define NULL 0 //要么是0
#else
#define NULL ((void *)0) //要么是(void *)0指针
#endif

在Ubuntu下执行结果如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KBPY8WKg-1639112293977)(C:\Users\Sean\AppData\Roaming\Typora\typora-user-images\image-20211123152717740.png)]

问题在这句:

    a=*node; 

对node进行*运算,node此时并没有因为函数fun有所改变,还是一个(void *)0指针,所以段错误就出来;

当然你可以读取NULL本身的值,即0,但是读取它指向的值,那是非法的,会引发段错误

ps:貌似这种指针的错误还有:操作系统限制用户访问的地址空间,内存木有分到的地址空间(几百KB的嵌入式系统中普遍存在),再加这种就有三种,当然野指针也可能乱指到一般用户合法的地址,然后就乱改,然后就失控了。

NULL是个好东西,给一出生的指针一个安分的家

基于NULL的定义,我们做这样的尝试:

#include <stdio.h>
 
int main()
{
    int *iPtr1 = NULL;
    int *iPtr2 = 0;
    //error: invalid conversion from `int' to `int*'
    int *iPtr3 = 1;
 
    //error: invalid conversion from `void*' to `int*'
    int *iPtr4 = ((void* )0);
    
    //void *是无类型的指针,可以强制转换成任何的类型
    //这样写就不会报错了
    int *iPtr4 = (int *)((void* )0);
    
    return 0;
}

回顾第一道题,写一个注释:

#include <stdio.h>  
  
void fun(int* node) // 这里的node不是主程序里的node,是一个保存在栈空间的指针变量node, 
{  // 接收的值是实参传递的值,相当于实参传递的值0给形参的指针变量node初始化,形参的node也指向0
    static int N = 100;  // N存储在静态存储区
    node = &N;  // 把N的地址赋给指针变量node。但是node是栈空间的一个指针变量
}  // 函数调用结束后,node就不存在了。所以这条赋值是没有意义的。
int main()  
{  
    int* node = NULL;  
    int a = 0;  
    fun(node);  // 指针node的值作为实参,所以传递的是0
    a = *node;  // 这里的node是主程序的node,其值还是NULL,对NULL取*操作造成段错误。
    printf("%d\n",a);  
      
    return 0;    
}

要想让程序1跑起来,做如下的修改:

(1)方法一:使用指针的指针——传递指针的地址,用指针的指针作接收。

#include <stdio.h>  
  
void fun(int** node) 
{  
    static int N = 100;  
    *node = &N;  
}  
int main()  
{  
    int* node = NULL;  
    int a = 0;  
    fun(&node);  
    a = *node;  
    printf("%d\n",a);  
      
    return 0;    
}

(2)方法二:使用return:

#include <stdio.h>  
  
int* fun() 
{  
	static int N = 100;  
	return &N; 		 
}  
int main()  
{  
    int* node = NULL;  
    int a = 0;  
    node = fun(node);  
    a = *node;  
    printf("%d\n",a);  
      
    return 0;    
}

2. 关于malloc,真的理解了吗?

一、基本说明

  • 原型:*extern void malloc(unsigned int num_bytes);

  • 头文件:#include <malloc.h> 或 #include <alloc.h> (注意:alloc.h 与 malloc.h 的内容是完全一致的。)

  • 功能:分配长度为num_bytes字节的内存块

  • 说明:如果分配成功则返回指向被分配内存的指针,否则返回空指针NULL。

  • 当内存不再使用时,应使用**free()**函数将内存块释放。

二、函数声明(函数原型):

void *malloc(int size);

说明:malloc 向系统申请分配指定size个字节的内存空间。返回类型是 void* 类型。void* 表示未确定类型的指针。C,C++规定,void* 类型可以强制转换为任何其它类型的指针。

三、malloc与new的不同点

从函数声明上可以看出。malloc 和 new 至少有两个不同:

  1. new 返回指定类型的指针,并且可以自动计算所需要大小。比如:
  int *p;
  p = new int; 
//返回类型为int* 类型(整数型指针),分配大小为 sizeof(int);

  或:

  int* parr;
  parr = new int [100]; 
//返回类型为 int* 类型(整数型指针),分配大小为 sizeof(int) * 100;

 而 malloc 则必须由我们计算要字节数,并且在返回后强行转换为实际类型的指针。

   int* p;
  p = (int *) malloc (sizeof(int));

第一、malloc 函数返回的是 void * 类型,如果你写成:p = malloc (sizeof(int)); 则程序无法通过编译,报错:“不能将 void* 赋值给 int * 类型变量”。所以必须通过 (int *) 来将强制转换。

第二、函数的实参为 sizeof(int) ,用于指明一个整型数据需要的大小。如果你写成:

int* p = (int *) malloc (1);

代码也能通过编译,但事实上只分配了1个字节大小的内存空间,当你往里头存入一个整数,就会有3个字节无家可归,而直接“住进邻居家”!造成的结果是后面的内存中原有数据内容全部被清空。

malloc 也可以达到 new [] 的效果,申请出一段连续的内存,方法无非是指定你所需要内存大小。

比如想分配100个int类型的空间:

int* p = (int *) malloc ( sizeof(int) * 100 ); //分配可以放得下100个整数的内存空间。

另外有一点不能直接看出的区别是,malloc 只管分配内存,并不能对所得的内存进行初始化,所以得到的一片新内存中,其值将是随机的。

除了分配及最后释放的方法不一样以外,通过malloc或new得到指针,在其它操作上保持一致。

总结:

malloc()函数其实就在内存中找一片指定大小的空间,然后将这个空间的首地址范围给一个指针变量,这里的指针变量可以是一个单独的指针,也可以是一个数组的首地址,这要看malloc()函数中参数size的具体内容。我们这里malloc分配的内存空间在逻辑上连续的,而在物理上可以连续也可以不连续。对于我们程序员来说,我们关注的是逻辑上的连续,因为操作系统会帮我们安排内存分配,所以我们使用起来就可以当做是连续的。

2.1 关于malloc(strlen(str)+1)问题

看看下面这一段代码,会输出什么?

#include <stdio.h>
#include <string.h>
 
int main(int argc,char const *argv[]) {
    char *b="hello";
    printf("%d\n",strlen(b));
    char *a=(char*)malloc(strlen(b)+1);
    printf("%d",strlen(a));
}

答案是:不确定。

因为malloc分配的内存内容是随机的;另一方面,strlen(a) 是获取实际的字符串长度,原理是遍历a数组直到找到一个’\0’字符为止。不是获取分配空间的长度。
a数组初始空间的内容是随机不确定的,遍历a数组什么时候找到’\0’字符就不确定了。如果printf("%d",strlen(a))之前先strcpy(a,b),那么结果就是确定的了

此外,关于为什么在用Malloc搭配strlen给字符指针(串)分配内存的时候字节大小要设置为(strlen(str)+1)的问题,原因在于strlen得出的结果是字符串的实际字符个数,而在内存中,字符串的末尾是会有一个\0结束符的。

到底free啥?

看个例子:

if(strstr(buf,"download")){
			char *tmpstr = (char *)malloc(strlen(buf)+1);
			char *org = tmpstr; 
			memset(tmpstr, 0, strlen(buf)+1);
			tmpstr = strcat(tmpstr ,strstr(buf, "download"));
			tmpstr += strlen("download");
			tmpstr += strspn(tmpstr," \t");
		
			
			char *filename = (char *)malloc(strlen(tmpstr));	
			strcpy(filename,tmpstr);
			strtok(filename," \n\t");
			puts(filename);
    		free(org);

2.2 关于怎么理解’\0’的问题

‘\0’ 是字符串的结束符,任何字符串之后都会自动加上’\0’。如果字符串末尾少了’\0’转义字符,则其在输出时可能会出现乱码问题。

‘\0’转义字符在ASCII表中并不表示阿拉伯数字0,阿拉伯数字0的ASCII码为48,’\0’转义字符的ASCII码值为0,它表示的是ASCII控制字符中空字符的含义。

‘\0’是C++中字符串的结尾标志,存储在字符串的结尾。比如char cha[5]表示可以放4个字符的数组,由于c/c++中规定字符串的结尾标志为’\0’,它虽然不计入串长,但要占内存空间,而一个汉字一般用两个字节表示,且c/c++中如一个数组cha[5],有5个变量,分别是 cha[0] , cha[1] , cha[2] , cha[3] , cha[4]。

所以cha[5]可以放4个字母(数组的长度必须比字符串的元素个数多1,用以存放字符串结束标志’\0’)或者放2个汉字(1个汉字占2个字节,1个字母占一个字节),cha[5]占5个字节内存空间。如果字符串末尾少了’\0’转义字符,则其在输出时可能会出现乱码问题。

2.3alloc和realloc

void *calloc( size t num_elements, size t element size );

calloc也用于分配内存。malloc和calloc之间的主要区别是后者在返回指向内存的指针之前把它初始化为0。这个初始化常常能带来方便,但如果你的程序只是想把一些值存储到数组中,那么这个初始化过程纯属浪费时间。calloc和malloc之间另一个较小的区别是它们请求内存数量的方式不同 calloc的参数包括所需元素的数量和每个元素的字节数。根据这些值,它能够计算出总共需要分配的内存。

void realloc( void *ptr, size_t new_size ); 

realloc 函数用于修改一个原先已经分配的内存块的大小。使用这个函数,可以使一块内存扩大或缩小。如果它用于扩大一个内存块,那么这块内存原先的内容依然保留,新增加的内存添加到原先内存块的后面,新内存并未以任何方法进行初始化。如果它用于缩小一个内存块,该内存块尾部。

注意:这个参数的类型size_t,它是一个无符号类型,定义于stdlib.h

3. C语言中怎么通过函数修改实参的值?

#include<stdio.h>
int fun(int x)
{
    x=9;
}
int main(void)
{
    int t=1;
    fun(t);
    printf("t = %d\n",t);
}

结果为 t = 1。
怎样通过调用函数来修改实参值呢?

方法一:用返回值的办法

#include<stdio.h>
int fun(int x)
{
    x=9;
    return x;
}
int main(void)
{
    int t=1;
    t = fun(t);
    printf("t = %d\n",t);
}

结果为 t = 9。

方法二:用指针(地址传递)的办法

#include<stdio.h>
int fun(int*x)
{
    *x=9;
}
int main(void)
{
    int t=1;
    fun(&t);
    printf("t = %d\n",t);
}

结果为 t = 9。

如果要通过函数来修改一个n维指针,那么要传递的就是一个n+1维的指针

#include <stdio.h>  
  
void fun(int** node)  //2. 用指针的指针来接收 
{  
    static int N = 100;  
    *node = &N;  
}  
int main()  
{  
    int* node = NULL;  
    int a = 0;  
    fun(&node);  //1. 传递指针的地址
    a = *node;  
    printf("%d\n",a);  
      
    return 0;    
}

对于字符串来说也是一样的,看一个例子:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void change1( char * str){
	str = "goodbye";
}


void change2( char **str){
	*str = "goodbye";
}


 
int main(){
	char *str="hello";
	puts(str);
	
	change1(str);
	puts(str);
	
	change2(&str);
	puts(str);
	
	return 0;
	
}

结果:
在这里插入图片描述

4. gets和fgets的区别?puts和fputs呢?

4.1 gets和fgets

在编程中发现gets和fgets一些区别总结一下;

1、 fgets比gets安全,使用gets编译时会警告

为了安全,gets少用,因为其没有指定输入字符的大小,限制输入缓冲区得大小,如果输入的字符大于定义的数组长度,会发生内存越界,堆栈溢出。后果非常怕怕

fgets会指定大小,如果超出数组大小,会自动根据定义数组的长度截断。

2、 用strlen检测两者的输入的字符串长度,结果不一样

gets:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SJV2sy3W-1639112293979)(C:\Users\Sean\AppData\Roaming\Typora\typora-user-images\image-20211123161035951.png)]

fgets:
在这里插入图片描述

可以看到,同样是输入123

gets只有一次换行,这是因为程序的语句printf(“%s\n”,str)

fgets有两次,而第二次是其本身把回车换行符存入了字符串里

所以,gets的长度只有3和输入的字符串长度一样,fgets是4,多出来的是回车换行符。

具体的介绍:

fgets函数fgets函数用来从文件中读入字符串。

1. fgets函数的调用形式如下:
fgets(str,n,fp);

2. 参数解释:
fp是文件指针;
str是存放在字符串的起始地址;
n是一个int类型变量,读出n-1个字符。

函数的功能是从fp所指文件中读入n-1个字符放入str为起始地址的空间内;如果在未读满n-1个字符之时,已读到一个换行符或一个EOF(文件结束标志),则结束本次读操作。

读入的字符串中最后包含读到的换行符。因此,确切地说,调用fgets函数时,最多只能读入n-1个字符。读入结束后,系统将自动在最后加'\0',并以str作为函数值返回。

gets()将删除新行符’\n’, fgets()则保留新行符。

要去掉fgets()最后带的“\0",只要用 s[strlen(s)-1]=’\0’;即可。

fgets不会像gets那样自动地去掉结尾的\n,所以程序中手动将\n位置处的值变为\0,代表输入的结束。

针对于fgets,还要再说两句,下面这种用法,是安全的判断文件读取结束或者出错的好方式,切忌不能使用while(!feof(fp)) ,还有对于fgets的第二个参数是最大能读取文件字符的个数,一般最大的长度是1024字节。

while(fgets(…, stream)){

? /* … */

}

if(ferror(stream)){

? /* … */

}

4.2 puts和fputs

1、函数原型
int puts(const char *s);
int fputs(const char *s, FILE *stream);

2、函数描述
函数fputs 将一个以null字节终止的字符串写到指定的流,尾端的终止符null 不写出。注意,这并不一定是每次输出一行,因为字符串不需要换行符作为最后一个非null字节。通常,在null字节之前是一个换行符,但并不要求总是如此。

puts将一个以null字节终止的字符串写到标准输出,终止符不写出。但是,puts随后又将一个换行符写到标准输出。

3、puts演示程序
我们知道,puts 函数主要用于向标准输出设备(屏幕)写入字符串并换行,即自动写一个换行符(’\n’)到标准输出。理论上,该函数的作用与“printf("%s\n",str);”语句相同。但是,puts 函数只能输出字符串,不能进行相关的格式变换。与此同时,它需要遇到 null(’\0’) 字符才停止输出。因此,非字符串或无 null(’\0’) 字符的字符数组最好不要使用该函数打印,否则无法正常结束。

#include <stdio.h>
#include <string.h>
int main(void)
{
    char str[] = {'H','E','L','L','O'};
    puts(str);
    return 0;
}

在Linux上运行程序

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LLxVHk2Y-1639112293980)(C:\Users\Sean\AppData\Roaming\Typora\typora-user-images\image-20211123162808553.png)]

在上面的示例代码中,因为字符数组 str 在结尾处缺少一个 null(’\0’) 字符(也就是说它不是一个严格意义上的字符串)。因此,在调用 puts 函数的时候,程序将不知道什么时候停止输出,从而导致输出结果未定义。

正确的做法是:在字符数组 str 的结尾处添加一个 null(’\0’) 字符,如下面的示例代码所示:

char str[] = {‘H’,‘E’,‘L’,‘L’,‘O’,’\0’};
4、fputs演示程序

#include <stdio.h>
#include <string.h>
int main(void)
{
    char str[] = {'H','E','L','L','O'};
    fputs(str,stdout);
    printf("\n");
    return 0;
}

运行程序,发现如果str数组最后不加 \0 同样会出错!,在最后加上 \0 就不会出错了。

5、总结
区别:puts随后又将一个换行符写到标准输出。

fputs、puts函数输出的字符串必须以 null 字节结尾(null字节并不输出),否则将出错。

puts并不像它所对应的gets那样不安全。但是我们还是应避免使用它,以免需要记住它在最后是否添加了一个换行符。如果总是使用fgets和fputs, 那么就会熟知在每行终止处我们必须 自己处理换行符 。

5. Makeargv为例子解析一下

该函数基于字符串s创建了一个由argvp指向的参数数组,这个数使用delimiters指定的界定符,若成功则返回标记的个数,若不成功则返回makeargv返回-1并置errno。

int makeargv(const char *s, const char *delimiters, char ***argvp)
{
        int error;
        int i;
        int numtokens;
        const char *snew;	//使用const做限定,不可修改
        char *t;

        if((s == NULL)||(delimiters == NULL)||(argvp == NULL )){
                error = EINVAL;
                return -1;	//错误退出
        }

        *argvp = NULL;
        snew = s + strspn(s,delimiters);
    /*
    strspn的作用是找出字符串s中连续n个字符属于字符数组delimiters任意字符的个数n;
    这一步是为了snew成为字符串s的真正的从起始位置开始的子串。
    注意这个时候,由于snew和s本质上都是指针,它们指向的还是同一片内存(除了起始地址有所区别)!
    */
    
        if( (t = malloc(strlen(snew) + 1)) == NULL ){
                return -1; //错误返回
        }
    /*
    这一步通过malloc给预先定义的 char *类型的指针t开辟了一段内存空间。
    由于strlen的机制是扫描一个字符串直到'\0'(字符串结束标识符),并返回扫描到的实际字符个数,且实际的字符串(除了实际字符还需要)以'\0'结尾,故在使用malloc搭配strlen给字符串分配内存空间的时候要使用strlen(str) + 1的大小。
    */
    

        strcpy(t,snew);
    /*
    将snew拷贝到字符串t中,留snew做备份防止原待解析字符串snew被修改;
    由于t已经开辟了一片自己的内存空间,且使用了strcpy函数(而不是等于号),
    故此时t和snew指向的并不是同一片地址空间,t是snew的一个备份。
    */
    
    
        numtokens = 0;
        if(strtok(t,delimiters) != NULL){
                for( numtokens = 1; strtok(NULL,delimiters)!=NULL; numtokens++);	//这一步主要是得出了numtokens,即子串(参数)个数。
        }
    /*
    strtok函数:是对字符串基于分界符delimiters的就地解析(在原内存地址上做解析和更改),将字符串t中分界符(注意是字符而不是子串)在原内存地址上修改为'\0';
    strtok每次只解析并修改一个被分界符分割出的子串;
    在第一次调用以后,若想对同一字符串的剩余部分继续解析,在后续调用中要将strtok的第一个参数置为NULL,(通过strtok默认保留的指针)从上一次分割的分界符的下一字节开始继续解析。
    返回值:若成功则返回修改后的子串的起始位置,若错误则返回NULL;
    */

        if((*argvp = malloc((numtokens + 1)*sizeof(char *))) == NULL){
                error = errno;
                free(t);   //在错误退出的时候不能忘记free!否则会可能造成内存泄漏
                errno = error;
                return -1;
        }
    /*
    给argvp指向的区域通过malloc开辟一段内存空间。(*argvp代表argvp指向的实际值)
    其大小要足够存储numtokens个参数子串的地址(char *类型),此外还要在末尾存一个NULL(shell的argc参数数组要求);
    故malloc的参数为:(numtokens + 1)*sizeof(char *))
    */

       if(numtokens == 0)
                free(t);
        else{
                strcpy(t,snew);
            /*
            上一次的strcpy是为了获取numtokens来做*argv的malloc;
            但是t经过了全串的strtok之后,每个子串的起始地址已经丢失了。
            这一次的strcpy是为了将**argv的值修改为(将*argv指向)各子串的起始地址。
            */
            
                **argvp = strtok(t, delimiters);
                for(i = 1; i < numtokens; i++)
                        *((*argvp)+i) = strtok(NULL, delimiters);
        }
        *((*argvp) + numtokens) = NULL;
        return numtokens;
}


[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9hpcb2Bt-1639112293981)(C:\Users\Sean\AppData\Roaming\Typora\typora-user-images\image-20211124105850212.png)]

关于为什么makeargv的参数是:char ***argvp(三维指针)而不是实际上的使用的char **charargv(二维指针)的问题:
    因为要想在函数中直接修改实参,则需要使用指针传值;
    若是实参是一个n维指针类型,那么在参数中就应该传递n+1维的指针参数;
    具体来说: (1)在调用时使用n维指针的地址去调用函数;
    		 (2)在函数声明中用n+1维的指针接收。
    
    如makeargc在实际调用时是这样写的:
    
//声明中用n+1维的指针接收
int makeargv(const char *s, const char *delimiters, char ***argvp);

int main(void) {
        char **chargv;
        char inbuf[MAX_BUFFER];

        for(;;){
                gets(inbuf);
                if(strcmp(inbuf,QUIT_STRING) == 0)
                        return 0;
                //调用时使用n维指针的地址去调用函数
                if((fork() == 0) && (makeargv(inbuf, " ", &chargv) > 0))
                        execvp(chargv[0], chargv);
                wait(NULL);     //waiting for the child process to begin another loop
        }
}


对比find_char()

可以与一个不改变源字符串的函数find_char做对比:

在这里插入图片描述

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>

size_t find_char( char ** string, int value){
	
    assert(string != NULL);
    
    while(*string != NULL){
        
        while(**string != '\0'){
            if (*(*string)++ == value )
                return true;
        }
        
        string ++;
    }
    return false;
}

断言——assert()

简单地讲,断言就是对某种假设条件进行检查。在 C 语言中,断言被定义为宏的形式(assert(expression)),而不是函数。其原型定义在<assert.h>文件中。

其中,assert 将通过检查表达式 expression 的值来决定是否需要终止执行程序。也就是说,如果表达式 expression 的值为假(即为 0),那么它将首先向标准错误流 stderr 打印一条出错信息,然后再通过调用 abort 函数终止程序运行;否则,assert 无任何作用。

6. C语言里关于字符串的那些函数

strchr

char *strchr(const char *s, int c);

它表示在字符串 s 中从前到后查找字符 c,返回字符 c 第一次在字符串 s 中出现的位置;

如果未找到字符 c,则返回 NULL。

例子:一个解析并提取shell中重定向源文件名并将标准输入重定向的函数

int parseandredirectin(char *cmd){
        int error;
        int infd;
        char *infile;
        if((infile = strchr(cmd,'<')) == NULL) /* where the < begin */
                return 0;
        *infile = 0;            /* take everything after < out of cmd */
        infile = strtok(infile+1, " "); /* manage the infile */
        if(infile == NULL)
                return 0;
        if((infd = open(infile, O_RDONLY)) == -1)
                return -1;
        if(dup2(infd,STDIN_FILENO) == -1){
                error = errno;
                close(infd);
                errno = error;
                return -1;
        }
        return close(infd);
}

strrchr

char *strrchr(const char *s, int c);

与 strchr 函数唯一不同的是,strrchr 函数在字符串 s 中是从后到前(或者称为从右向左)查找字符 c,找到字符 c 第一次出现的位置就返回,返回值指向这个位置。

strtok

功能:作用于字符串s,以delim中的字符为分界符,将s切分成一个个子串;如果,s为空值NULL,则函数保存的指针SAVE_PTR在下一次调用中将作为起始位置。

返回值:若成功则返回第一个修改后的子串的起始位置,若错误则返回NULL;

char *strtok(char *s, char *delim)

strtok函数:是对字符串基于分界符delimiters的就地解析(在原内存地址上做解析和更改),将字符串t中分界符(注意是字符而不是子串)在原内存地址上修改为’\0’;

strtok每次只解析并修改一个被分界符分割出的子串;

在第一次调用以后,若想对同一字符串的剩余部分继续解析,在后续调用中要将strtok的第一个参数置为NULL,(通过strtok默认保留的指针)从上一次分割的分界符的下一字节开始继续解析。

注意:strtok函数的作用是分解字符串,所谓分解,即没有生成新串,只是在s所指向的内容上做了些手脚而已。因此,源字符串s发生了变化!

strspn

size_t strspn(char const *str, char const *grouo);

计算字符串 str 中连续有几个字符都属于字符串 accept,其返回值是字符串str开头连续包含字符串accept内的字符数目,即:如果 str 所包含的字符都属于 accept,那么返回 str 的长度;如果 str 的第一个字符不属于 accept,那么返回 0。

strcspn

用来检索字符串s1开头连续有几个字符都不含字符串s2中的字符,其原型为:

int strcspn(char *s1, char *s2);

s1、s2为要进行查找的两个字符串。

strcspn()从字符串s的开头计算连续的字符,而这些字符都完全不在字符串s2中。简单地说,若strcspn()返回的数值为n,则代表字符串s1开头连续有n 个字符都不含字符串s2中的字符。

【返回值】返回字符串s1开头连续不含字符串s2内的字符数目。

注意:strcspn()会检查字符串结束标志’\0’;如果s1中的字符都没有在s2中出现,那么将返回s1的长度;检索的字符是区分大小写的。

sprintf

用于格式化字符串。

int sprintf( char *buffer, const char *format [, argument]);

除了前两个参数类型固定外,后面可以接任意多个参数。而它的精华,显然就在第二个参数:格式化字符串上。

例如:

sprintf(s, "%d", 123); // 产生"123"

snprintf

int snprintf(char*str, size_t size,constchar*format, ...);

函数说明:最多从源串中拷贝 n-1 个字符到目标串中,然后再在后面加一个 ‘\0’。所以如果目标串的大小为 n 的话,将不会溢出。

函数返回值:若成功则返回欲写入的字符串长度,若出错则返回负值。

strcat

char *strcat(char *dest, const char *src)

dest – 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串。

src – 指向要追加的字符串,该字符串不会覆盖目标字符串。

值得注意的几个例子:

  1. #include <iostream>
     using namespace std;
     #include <stdio.h>
     #include <string.h>
     
     int main()
     {
             char *p = "welcome ";
             char *q = "to the house";
             strcat(p, q);
             cout<<p<<endl;
             return 0;
     }
    /*
    
     第一个程序中,采用的指针变量p存储第一个字符串。运行过程中,指针变量p所指向的内存是分配在堆中的,且只分配了足够其指向的内容的内存。将q连接到p后,自然p是没有其他空间给q了,所以出现了内存写入冲突而中断。
     
    */
    
    
    同理,下面这段代码也是错误的
        
    #include <iostream>
    using namespace std;
    #include <stdio.h>
    #include <string.h>
     
    int main()
    {
            char p[] = "welcome ";
            char *q = "to the house";
            strcat(p, q);
            cout<<p<<endl;
            return 0;
    }
    /*
    
    第二个程序中,采用未指定大小的数组p存储第一个字符串。运行过程中,数组p存放在栈中,因其未指定大小,那么在初始化时,系统就默认其指定大小为初始化字符串的长度。将q连接到p后,p也没有多余的空间给q,所以就出现了上述图中的错误。
    
    */
    
    

    结果自然无法执行,越界了。

    做如下的修改,给p开辟一段空间:

    #include <stdio.h>
    #include <string.h>
    
    int main()
    {
            char *p = (char *)malloc(255);
    	//	p = "welcome "; 
        //这样的写法是非法的,这样赋值的话malloc就被闲置了,转而指向"welcome"的地址
    		strcpy(p,"welcome");
            char *q = "to the house";
            strcat(p, q);
            puts(p);
            free(p);
            return 0;
    }
    

总结:strcpy其作用为去掉字符串a的结束符"\0"后,将字符串b连接到a的后面,并存储到a中。那么a就要有足够的存储空间,否则就会溢出。

strcpy

char *strcpy(char *dest, const char *src) 

把 src 所指向的字符串复制到 dest。

需要注意的是如果目标数组 dest 不够大,而源字符串的长度又太长,可能会造成缓冲溢出的情况。

下面的语句是错误案例:

char *str;
strcpy(str,"The C of Tranquility");

strcpy()把

"The C of Tranquility"

拷贝到str指向的地址上,但是str未被初始化,所以该字符串可能被拷贝到任意的地方。

strcpy()的其它属性:
strcpy()的返回类型是char *,该函数返回的是一个字符的地址。
第一个参数不必指向数组的开始,这个特性可用于拷贝数组的一部分。

和strcat一样,需要注意数组越界问题。

例子:

void get_cur_time(char *time_str)
{
    struct timeval now;
    gettimeofday(&now, NULL);
    strcpy(time_str, ctime(&now.tv_sec));
}

char buf[64];
gets(buf);
if(strstr(buf,"time")||strstr(buf,"Time")){
			char time_str[30];
			get_cur_time(time_str);
			//memset(buf, 0, strlen(buf));
			strcpy(buf, "Current time ---- ");
			strcat(buf, time_str);
			strcat(buf,"\n");
			puts(buf);
}

strncpy

按字符赋赋值字符串。

char *strncpy(char *dest, const char *src, size_t n)

参数说明:

dest – 指向用于存储复制内容的目标数组

src – 要复制的字符串

n – 要从源中复制的字符数

strcmp

比较字符串s1和s2。

int strcmp(const char *s1,const char *s2);

s1– 指向字符串的指针
s2– 指向字符串的指针

  • 返回值
    自左向右逐个按照ASCII码值进行比较,直到出现不同的字符或遇’\0’为止。
    如果返回值 < 0,则表示 s1 小于 s2。
    如果返回值 > 0,则表示 s1 大于 s2。
    如果返回值 = 0,则表示 s1 等于 s2。

例子:

#include <stdio.h>
#include <string.h>	

int main(void)
{
	char *a = "English";
    char *b = "ENGLISH";
    char *c = "english";
    char *d = "English";
    

//strcmp()只能比较字符串, 其他形式的参数不能比较 
printf("strcmp(a, b):%d\n", strcmp(a, b));//字符串之间的比较 
printf("strcmp(a, c):%d\n", strcmp(a, c));
printf("strcmp(a, d):%d\n", strcmp(a, d));
printf("strcmp(a, \"English\"):%d\n", strcmp(a, "English"));
printf("strcmp(&a[2], \"glish\"):%d\n", strcmp(&a[2], "glish")); 
return 0;

}

运行结果如下:

strcmp(a, b):1
strcmp(a, c):-1
strcmp(a, d):0
strcmp(a, "English"):0
strcmp(&a[2], "glish"):0

strncmp

按字节的strcmp。

int strncmp(const char *str1, const char *str2, size_t n)
  • 参数:
    str1 – 要进行比较的第一个字符串。
    str2 – 要进行比较的第二个字符串。
    n – 要比较的最大字符数。

  • 返回值:
    按照ASCII值进行比较,str1-str2的数值就是返回值。
    如果返回值 < 0,则表示 str1 小于 str2。
    如果返回值 > 0,则表示 str2 小于 str1。
    如果返回值 = 0,则表示 str1 等于 str2。

下面这两句代码等效:

if(temp[0]!='q');
if(strncmp(temp,"q",1)!=0);

strstr

定义在<string.h>当中,用于在字符串中查找一个子串第一次出现的起始位置,并返回这个地址。

如果不含该字串,将返回NULL;如果子串参数为空串,那么返回母串。

char *strstr(char const *s1, char const *s2);

strpbrk

C 库函数 strpbrk 检索字符串 str1 中第一个匹配字符串 str2 中字符的字符,不包含空结束字符。也就是说,依次检验字符串 str1 中的字符,当被检验字符在字符串 str2 中也包含时,则停止检验,并返回该字符位置。

char *strpbrk(const char *str1, const char *str2)
  • str1 – 要被检索的 C 字符串。
  • str2 – 该字符串包含了要在 str1 中进行匹配的字符列表。

该函数返回 str1 中第一个匹配字符串 str2 中字符的字符数,如果未找到字符则返回 NULL。

例子:

#include <stdio.h> 
#include <string.h>  

int main () {   
    const char str1[] = "abcde2fghi3jk4l";   
    const char str2[] = "34";  
    char *ret;    
    ret = strpbrk(str1, str2);   
    if(ret){      
        printf("第一个匹配的字符是: %c\n", *ret);   
    }   
    else    
    {      
        printf("未找到字符");   
    }      
    return(0); 
}

输出结果:第一个匹配的字符是: 3

7. 深入理解指针

内存的概念

做简单补充:

内存分为两个部分(1)物理存储器(内存条) (2)存储地址空间(逻辑的,程序员关注的)

内存作为cpu和硬盘的桥梁。

字/逻辑地址单元

每个字节包含8位比特 —— 可以存储无符号值0255,或者有符号值-128127.

为了存储更大的值,我们把两个或多个(四个)字节合在一起作为一个更大的存储单位,即逻辑地址单元。

比如一个四个字节的字,其可容纳的无符号数据范围扩大到从0~4294967295(2^32 - 1)。

注意,尽管一个字包含了四个字节,但是它仍然只有一个地址。

未初始化和非法指针

下面这个代码段说明了一个极为常见的错误:

int *a; 
*a = 12;

正确的写法:
    int *a;
	a = (int *)malloc(int);
	*a = 12;

这个声明创建了一个名叫a的指针变量,后面那条赋值语句把12存储在a所指向的内存位置。

警告:
但是究竟a指向哪里呢?我们声明了这个变量,但从未对它进行初始化,所以没有办法预测 12这个值将存储于什么地方。从这一点看,指针变量和其他变量并无区别。如果变量是静态的,它会被初始化为0;如果变量是自动的,它根本不会被初始化。无论是哪种情况,声明一个指向整型的指针都不会“创建”用于存储整型值的内存空间。

所以,如果程序执行这个赋值操作,会发生什么情况呢?如果运气好,a的初始值会是个非法地址,这样赋值语句将会出错,从而终止程序。在UNIX系统上,这个错误被称为“段违例”(segmentation violation)或“内存错误”(memoryfault)。它提示程序试图访问一个并未分配给程序的内存位置。在一台运行Windows的PC上,对未初始化或非法指针进行间接的访问操作是一般保护性异常(general protection exception)的根源之一。

对于那些要求整数必须存储于特定边界的机器而言,如果这种类型的数据在内存中的存储地址处于错误的边界上,那么对这个地址进行访问时将会产生一个错误。这种错误在UNIX系统中被称为“总线错误”(bus error)。

一种更为严重的情况是,这个指针可能偶尔包含了一个合法的值,位于那个地方的值很容易被修改。

指针常量

让我们来分析另外一个表达式。假定变量a存储于位置 100,下面这条语句的作用是什么?
*100=25;
它看上去像是把25赋值给a,因为a是位置100所存储的变量。但是,这是错的!这条语句实际上是非法的,因为字面值100的类型是整型,而间接访问操作只能作用于指针类型表达式。如果你确实想把25存储于位置100,就必须使用强制类型转换。
*(int *)100=25;
强制类型转换把值100从“整型”转换为“指向整型的指针”,这样对它进行间接访问就是合法的。如果a存储于位置100,那么这条语句就把值25存储于a。但是,你需要使用这种技巧的机会是绝无仅有的!为什么?前面提到,通常无法预测编译器会把某个特定的变量放在内存中的什么位置,所以无法预先知道它的地址。用&操作符得到变量的地址是很容易的,但表达式在程序执行才会进行求值,此时已经来不及把它的结果作为字面值常量复制到源代码。

这个技巧唯一有用之处是你偶尔需要通过地址访问内存中某个特定的位置时,它并不是用于问某个变量,而是访问硬件本身。例如,操作系统需要与输入输出设备控制器通信,它将启动I操作并从前面的操作中获得结果。在有些机器上,与设备控制器的通信是通过在某个特定内存地址读取和写入值来实现的。但是,与其说这些操作访问的是内存,还不如说它们访问的是设备控制器接口。这样,这些位置必须通过它们的地址来访问,此时这些地址是预先已知的。
并没有一种内建的记法用于书写指针常量。在那些极其罕见的需要使用它们的时候,它们通常写成整型字面值的形式,并通过强制类型转换转换成适当的类型。

例子:

#include <stdio.h>
#include <string.h>
#include<stdlib.h>

int main()
{
        char ch = 'a';
        char * ptr = &ch;
        printf("%p\n",ptr);
        printf("%p\n",&ch);
    
        printf("%c\n", *ptr);
        printf("%c\n",ch);
}

在这里插入图片描述

指针的指针

通过一个最简单的例子来说明:

int a = 12;
int *b = &a;
int **c = &b;
表达式相当的表达式
a12
b&a
*b12, a
c&b
*cb, &a
**c*b, a, 12

指针运算

先看一段程序,看看 各阶段都输出什么:

#include <stdio.h>
#include <string.h>
#include<stdlib.h>

int main()
{
        char * str = (char *)malloc(255);
        strcpy(str,"hello world");
        printf("%s\n", str);	// 输出 hello world
    
        
        char *tmp;
        tmp = str+1;
        puts(tmp);	//输出ello world
        
        *str = *str+1;
        puts(str);	//输出iello world(*str就是str[0]的意思,H的ASSIC码+1,变成了i)
        
        *(str+1) = *(str+1)+1;
        puts(str);	//输出ifllo world(*(str+1)就是str[1]的意思,e的ASSIC码+1,变成了f)
       
        printf("%s\n", (str+6));	//输出world
        
        free(str);
}

再来看一个strlen的函数自定义简版实现:

#include <stdlib.h>
size_t strlen(char *string)
{
	int len = 0;
    
    while(*string++ != '\0')
        len += 1;
    
    return length;
}

对比find_char():

可以应一个不改变源字符串的函数find_char做对比:

在这里插入图片描述

#include<stdio.h>
#include<assert.h>
#include<stdlib.h>

size_t find_char( char ** string, int value){

	
    assert(string != NULL);
    
    while(*string != NULL){
        
        while(**string != '\0'){
            if (*(*string)++ == value )
                return true;
        }
        
        string ++;
    }
    return false;
}

上面的程序包含了一些涉及指针值和整型值加法运算的表达式。是不是对指针进行任何运算都是合法的呢?答案是它可以执行某些运算,但并非所有运算都合法。除加法运算之外,还可以对指针执行一些其他运算,但并不是很多。

指针加上一个整数的结果是另一个指针。问题是,它指向哪里?

如果将一个字符指针加1,运算结果产生的指针指向内存中的下一个字符。float 占据的内存空间不止1字节,如果将一个指向 float的指针加 1,将会发生什么呢?它会不会指向该 float 值内部的某个字节呢?

幸运的是,答案是否定的。当一个指针和一个整数量执行算术运算时,整数在执行加法运算前始终会根据合适的大小进行调整。

这个“合适的大小”就是指针所指向类型的大小,“调整”就是把整数值和“合适的大小”相乘。

为了更好地说明,试想在某台机器上,float占据4字节。在计算 float型指针加3的表达式时,这个3将根据 float类型的大小(此例中为4)进行调整(相乘)。这样,实际加到指针上的整型值为12。

把3与指针相加使指针的值增加3个float 的大小,而不是3字节。这个行为较之获得一个指向一个 float 值内部某个位置的指针更为合理。

  C++知识库 最新文章
【C++】友元、嵌套类、异常、RTTI、类型转换
通讯录的思路与实现(C语言)
C++PrimerPlus 第七章 函数-C++的编程模块(
Problem C: 算法9-9~9-12:平衡二叉树的基本
MSVC C++ UTF-8编程
C++进阶 多态原理
简单string类c++实现
我的年度总结
【C语言】以深厚地基筑伟岸高楼-基础篇(六
c语言常见错误合集
上一篇文章      下一篇文章      查看所有文章
加:2021-12-11 15:32:36  更:2021-12-11 15:32:45 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 11:37:53-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码