C语言再学习笔记 Part.1

Posted 狱典司

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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下执行结果如下:

问题在这句:

    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:

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上运行程序

在上面的示例代码中,因为字符数组 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阅读笔记《C程序员 从校园到职场》第六章  常用文件操作函数 (Part 1)

C语言学习笔记C语言函数执行成功时,返回1和返回0,究竟哪个好?

我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段

我的C语言学习进阶之旅解决 Visual Studio 2019 报错:错误 C4996 ‘fscanf‘: This function or variable may be unsafe.(代码片段

c 语言学习笔记基础

《软件调试修炼之道》Part 1(CH1~5)读书笔记 PB16110698 第八周(~4.26)