C语言学习第017课——C语言提高(一)

本文详细介绍了C++中typedef的使用,以及指针和数据类型的相互作用。讨论了如何通过typedef创建类型别名以解决平台间的兼容问题。此外,还讲解了void类型的用途,特别是在函数返回和参数限定上的应用。接着,深入探讨了有符号数和无符号数的运算规则,以及可能出现的意外结果。文章还提到了局部变量和返回值的关系,指出栈中变量的生命周期和作用范围。最后,简述了内存管理,包括栈和堆的内存分配,以及野指针的概念和预防措施。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

typedef的用法

1、定义指针类型

定义两个char*的变量,p1和p2,使用C++代码打印一下他俩的类型:

#include <iostream>
#include<typeinfo>

using namespace std;

int test(){
    char* p1,p2;
    cout<< typeid(p1).name() <<endl;
    cout<< typeid(p2).name() <<endl;
}
int main()
{
    test();
    return 0;
}

运行结果:
在这里插入图片描述
这是为什么呢?
定义指针还有一种写法:

char *p1;

所以也就可以理解为,char修饰的是p1和p2, “*” 修饰的才是指针
如果想让两个打印结果都是Pc的话,应该这样定义:

int test(){
    char *p1,*p2;
    cout<< typeid(p1).name() <<endl;
    cout<< typeid(p2).name() <<endl;
}

或者直接用typedef将char* 起一个别名,原理和上面代码一样的

#include <iostream>
#include<typeinfo>

using namespace std;

typedef char* PCHAR;

int test(){
    PCHAR p1,p2;
    cout<< typeid(p1).name() <<endl;
    cout<< typeid(p2).name() <<endl;
}

2、定义特殊类型

例如:在一个平台上编译代码,需要用到long long 类型,而且用到不少这样的类型,但是在其他平台编译代码,库里面不支持这个long long类型,只支持int类型,这个时候只需要使用typedef给long long类型起一个别名

typedef long long MYLONG;

在之后需要定义long long类型的数据的时候,直接使用MYLONG定义
如果需要在另一个平台编译,只需修改:

typedef int MYLONG;

这一行就OK了

void数据类型的用法

void字面意思是无类型,void* 无类型指针,无类型指针可以指向任何类型的数据,
void定义变量是没有任何意义的,当你定义void a,编译器会报错,因为编译器不知道分配多少内存给变量。
void真正用在以下两个方面:

  • 对函数返回的限定
  • 对函数参数的限定

编译器不知道分配多少内存的情况

上面说到一个使用void定义变量,编译器报错,以为不知道分配多少内存
还有一种情况

struct Person{
    char name[64];
    int age;
    struct Person p;
};

这种嵌套本身的结构体,编译器也不知道分配多少内存,因为如果一直往里循环的话,人自己都算不出来要分配多少内存。

有符号数和无符号数的运算和转换

首先说一下,有符号数和无符号数从表面上来看,是声明的时候 一个有unsigned一个没有,但是归根结底是从二进制来看的
在内存中,所有的数都是以反码的形式存放的
例如:

int a = 1;			二进制:	00000000 00000000 00000000 00000001
有符号正数,原码,反码,补码都是他本身,最高位0代表是正数
int b = -1;			原码:	10000000 00000000 00000000 00000001
					反码:	11111111 11111111 11111111 11111110
					补码:	11111111 11111111 11111111 11111111
有符号负数,原码最高位1代表负数,剩余数代表数字,反码,除符号位之外都取反,
补码:在反码的基础上+1
unsigned int c = 1; 二进制:	00000000 00000000 00000000 00000000
无符号数:没有正负之分,也就不存在符号位,所有的32bit全部表示数值

所以在运算过程中,要从二进制的层面上来考虑

1、int和unsigned int 进行(逻辑)运算

#include <iostream>
using namespace std;

int test(){
    int a = -1;
    unsigned int b = 16;
    if(a>b){
        cout<<"负数大于正数了!"<<endl;
    }
    return 0;
}
int main()
{
    test();
    return 0;
}

运行结果:
在这里插入图片描述
知识点:有符号数和无符号数进行运算的时候,首先要将有符号数换算成无符号数,运算结果也是无符号数。
也就是说,-1最前面的符号位不按符号算了,算成数字的一部分

-1 		补码:	11111111 11111111 11111111 11111111
16		补码:	00000000 00000000 00000000 00010000

所以 很明显,两个都算成无符号数的话,-1比16大多了,所以会有上面的计算结果
练习:

#include <iostream>
using namespace std;

int test(){
	 int a = -1;
     unsigned int b = 16;
     cout<<a + b<<endl;	//15
     
     int c = -16;
     unsigned int d = 15;
     cout<<c + d<<endl;	//4294967295
     return 0;
}
int main()
{
    test();
    return 0;
}

分析:a+b 有符号数和无符号数进行运算,先换成无符号数

a=-1	原码:10000000 00000000 00000000 00000001
		反码:11111111 11111111 11111111 11111110
		补码:11111111 11111111 11111111 11111111
unsigned int b = 16
		补码:00000000 00000000 00000000 00010000	
相加结果		 00000000 00000000 00000000 00001111  = 15
----------------------------------------------------------
c = -16 原码:10000000 00000000 00000000 00010000
		反码:11111111 11111111 11111111 11101111
		补码:11111111 11111111 11111111 11110000
unsigned int d = 15
		补码:00000000 00000000 00000000 00001111
c+d		     11111111 11111111 11111111 11111111 = 4294967295(无符号数)

2、unsigned char和char运算

#include <iostream>
using namespace std;

int test(){
	 char a = -16;
	 unsigned char b = 14;
	 if(a>b){
        cout<<"负数大于正数了!"<<endl;	//没打印
	 }
	 cout<<a+b<<endl;		//-2
     return 0;
}
int main()
{
    test();
    return 0;
}

这个运行结果出乎意料,是因为什么呢?
知识点:C语言中比int小的整型(包括short 、unsigned short 、 unsigned char和char)在运算中都要转换成int然后进行运算,至于为什么选择转换为int,应该是从效率上考虑的,因为通常情况下int的长度被定义为机器处理效率最高的长度,比如32位机上,一次处理4个字节效率是最高的,所以虽然short更节省内存,但是在运算中的效率,是int更高。所以上面,无论是逻辑运算a>b还是算术运算a+b中a和b都默认转换成了int,不是unsigned int!转换成了int,不是unsigned int!转换成了int 不是unsigned int!

如果是unsigned的类型转换成int类型,高位补0.
如果是signed的类型转换成int类型,如果原来最高位是1则补1,如果是0则补0。

那么上面的结果就解释的通了:

char a = -16
	原码:	10010000
	反码:	11101111
	补码:	11110000		因为是有符号数,最高位是1,所以变成int类型,前面补1
int a =     11111111 11111111 11111111 11110000

unsigned char b = 14
	补码:	00001110		因为是无符号数,最高位不管是什么,直接补0
int b = 	00000000 00000000 00000000 00001110

int类型a为负数,b为正数,所以第一行打印不会出来
a+b =       11111111 11111111 11111111 11111110		这是个负数,要获取到他的值,需要取反,再+1
	取反:	10000000 00000000 00000000 00000001
	+110000000 00000000 00000000 00000010 	=-2

练习:

#include <iostream>
using namespace std;

int test(){
	 char a = -16;
	 unsigned char b = 255;
	 char c = 255;
	 cout<<a+b<<endl;//239
     cout<<a+c<<endl;//-17
     return 0;
}
int main()
{
    test();
    return 0;
}

分析:

char a = -16
换成int类型 
			11111111 11111111 11111111 11110000
unsigned char b = 255
	补码:	11111111
换成int类型(是unsigned类型,所以前面直接补000000000 00000000 00000000 11111111
char c = 255
	补码:	11111111
换成int类型(是signed类型,最高位是1,所以前面直接补111111111 11111111 11111111 11111111
-------------------------------------------------------
a+b			00000000 00000000 00000000 11101111	=239
a+c			11111111 11111111 11111111 11101111 	负数
	取反:	10000000 00000000 00000000 00010000
	+110000000 00000000 00000000 00010001 = -17

局部定义的变量不能当做返回值

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

char* getStr(){
    char str[] = "hello world!";
    return str;
}
int main(void){
    char* s = NULL;
    s = getStr();
    printf("s = %s\n",s);
}

以上代码,一个函数里面定义了一个字符串,然后直接返回了,在主函数中取到这串字符串,然后打印,很简单,但是运行结果是s=(null),为什么呢?
因为在函数中定义的字符串是保存在栈中的,函数一结束,栈中的内容就被回收了,在main函数中就无法打印出hello world了,
有人说字符串名称str从一定意义上来说他是一个指针,这样返回指针也不行吗?
那我们来说一下这几行代码的总体流程:
首先以

char str[] = "hello world!";

这种方式定义的字符串,hello world 是存储在常量区的,这一行代码的意思是,从常量区中将“hello world”复制到栈区,然后复制过来的第一个位置的指针赋值给str,函数中返回的str,其实返回的就是这个指针,函数结束,意味着复制过来的hello world被回收,而str对应的指针,也仅仅就是一个地址了,里面的内容已经不存在了。所以打印的时候会出现NULL。

指针的间接赋值

有了上面的例子,来看一下下面的代码,看一下打印结果是什么

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

void mallocSpace(char* p){
    p = (char*)malloc(100);
    memset(p,0,100);
    memcpy(p,"hello world",100);
}
int main(void){
    char* p = NULL;
    mallocSpace(p);
    printf("s = %s\n",p);
}

运行结果为:s=(null)
看起来代码没有什么问题,main中定义一个指针,将指针传递到函数中,给指针赋值,在main中再打印指针指向的字符串,也不存在方法出栈回收内存的问题,但是为什么会打印出来这样的结果呢?
我们来捋一捋程序在内存中的过程:
首先第一行,定义了一个char类型的指针,指向了NULL
第二行,将指针p传给了函数,进入函数,注意:此时的这个函数中的p和main中的p已经不是同一个p了(不信你打印一下两个p的地址一样不一样)
进入函数,内存会在栈中加载函数体,加载参数p(新加载出来的),而且在函数中操作的p全部都是这个,跟main中的p一点关系都没有了,所以最后打印出来会是null
为了证明,我自己加了两个打印:
在这里插入图片描述
既然明白了问题出在哪里,解决办法就是,要让函数参数穿进去的p,和main中的p一样了,那就传p的地址吧,p已经是一个指针了,相应的函数参数应该是一个二级指针。

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

void mallocSpace(char** p){
    printf("func    %p \n",*p);//00000000
    *p = (char*)malloc(100);
    printf("func    %p \n",*p);//00772BA8
    memset(*p,0,100);
    memcpy(*p,"hello world",100);
}
int main(void){
    char* p = NULL;
    printf("main    %p \n",p);//00000000
    mallocSpace(&p);
    printf("main    %p \n",p);//00772BA8
    printf("s = %s\n",p);//hello world
}

const全局变量和局部变量的区别

const全局变量在常量区,不能修改,用指针也不能修改。
const局部变量存放在栈上,不能直接修改,可以使用指针修改。

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

const int a = 100;//const修饰的全局变量,存放在常量区,不能直接或者间接修改

int main(void){
    const int b = 200;//const修饰的局部变量,存放在栈上,不能直接修改,可以间接修改
}


宏函数

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

#define MYADD(x,y) ((x)+(y))

int add(int x,int y){
    return x+y;
}
int main(void){
    int a = 10;
    int b = 20;
    printf("a + b = %d\n",MYADD(a,b));
}

以上代码显示的,add是一个正常的函数,MYADD就是一个宏函数了
说他是宏函数,其实他不是一个真正的函数
add函数才是真正的函数,因为他有返回值类型,参数,函数体
宏函数在一定场景下效率要比函数高
下面我们来分析一下,是在什么场景下,效率怎么个高的:
首先说普通函数调用流程:
假如,我在main中调用add函数计算结果,从main的第一行开始,过程为:

栈中开辟a,赋值为10-->
栈中开辟b,赋值为20-->
遇到了函数-->
返回地址(记录代码运行到这里了,等会运行完函数要继续跳回到这里的下一行),并跳转进入函数-->
栈中开辟y,并赋值为20-->
栈中开辟x,并赋值为10(此处函数的参数是从右向左陆续压栈的)-->
运算结果并存储在寄存器中,返回。
函数结束,x出栈,y出栈(这里具体是谁安排他们出栈,也不一定,后面说)

而宏函数没有这么复杂,他只是在预处理的时候,进行简单的替换文本,啥意思呢?我们之前学过编译四部曲:预处理,编译,汇编,链接。预处理的过程为:不检查语法,宏定义展开,头文件展开,就是这一步里面,他会把代码中的

printf("a + b = %d\n",MYADD(a,b));

直接替换为:

printf("a + b = %d\n",((a)+(b)) );

就是这么简单。省去了调用函数的时候,在栈里面的一切开销,整个过程相当于把宏定义的内容搬过来了,增加了代码量,是典型的空间换时间
所以说,对于频繁使用,并且短小的函数,我们一般使用宏函数代替,因为宏函数没有普通函数的开销(压栈,跳转,返回等)

函数的调用惯例

以上,我们了解了一个函数的调用流程,但是存在两个问题:
一个问题是上面的例子中函数的两个参数,是哪个先入栈?
二一个问题是,最后函数结束,参数出栈,是谁管他们出栈的,我们说函数的调用者也可以,函数本身自己也可以,那么到底是由什么决定的呢?

其实,函数的调用者和被调用者对函数的调用有着一致的理解,例如,他们双方都一致的认为函数的参数是按照某个固定的方式压入栈中,如果不这样的话函数将无法正确运行。
如果函数的调用方,在传递参数时,先压入a参数,再压入b参数,而被调用的函数则认为先压入的b,再压入a,那么被调用函数在使用ab的值时,就会颠倒。

因此函数的调用方和被调用方对于函数是如何调用的必须有一个明确的约定,只有双方都遵循一样的约定,函数才能被正确的调用。这样的约定被称为“调用惯例”
,一个调用惯例一般包扩以下几个方面:

函数参数的传递顺序和方式

函数的传递有很多种方式,最常见的是通过栈传递,函数的调用方将参数压入栈中,函数自己再从栈中将参数取出,对于有多个参数的函数,调用惯例要规定函数调用方将参数压栈的顺序,从左到右,还是从右到左,有些调用惯例还允许使用寄存器传递参数,以提高性能

栈的维护方式

在函数将参数压入栈中之后,函数体会被调用,此后需要将被压入栈中的参数全部弹出,以使得栈在函数调用前后保持一致,这个弹出的工作可以由函数的调用方来完成,也可以由函数本身来完成
为了在链接的时候对调用惯例进行区分,调用惯例要对函数本身的名字进行修饰,不同的调用惯例有不同的名字修饰策略。
事实上,在C语言中,存在着多个调用惯例,而默认的是cdecl,任何一个没有显示指定调用惯例的函数都默认是cdecl惯例,比如我们上面对于add函数的声明,他的完整写法应该是:

int _cdecl add(int x,int y){
    return x+y;
}

注意:_cdecl不是标准的关键字,在不同的编译器里可能有不同的写法,例如gcc里就不存在_cdecl这样的关键字,而是使用_attribute_((cdecl))

调用惯例出栈方参数传递名字修饰
cdecl函数调用方从右至左参数入栈下划线+函数名
stdcall函数本身从右至左参数入栈下划线+函数名+@+参数字节数
fastcall函数本身前两个参数由寄存器传递,其余参数通过堆栈传递@+函数名+@+参数的字节数
pascal函数本身从左至右参数入栈较为复杂,详见相关文档

C++里面的调用惯例是thiscall

变量传递分析

在这里插入图片描述
上图表示在main函数里面调用了A函数,而A函数又调用了B函数,那么,有几个情况:
main函数在栈区定义了一个变量,函数A和函数B都可以使用
函数A在栈区定义了一个变量,main不可以使用,函数B可以使用
函数B在栈区定义了一个变量,main和函数A都不可以使用
归纳为:在栈中定义的变量,只要还没有销毁,就可以被别的函数使用
如果任意一个函数在堆区定义了一个变量,只要没有free掉,任何函数都可以使用。

内存的存放方向

在这里插入图片描述
栈是开口向下的,越往里面放东西,地址是越来越低的

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

int main(void){
    int a = 10;
    int b = 20;
    int c = 30;
    int d = 40;
    printf("%p\n",&a);
    printf("%p\n",&b);
    printf("%p\n",&c);
    printf("%p\n",&d);
}

在这里插入图片描述
知道了栈的存放方向,那么内存的存放方向是什么样的呢?换句话说,有一个int类型的数0xaabbccdd,在内存中占4个字节,那这4个字节在内存中是按照aa bb cc dd这样排的呢?还是按照dd cc bb aa这么排的呢?

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

int main(void){
    int a = 0xaabbccdd;
    unsigned char* pchar = &a;
    printf("%x\n",*pchar);			//dd
    printf("%x\n",*(pchar+1));		//cc
    printf("%x\n",*(pchar+2));		//bb
    printf("%x\n",*(pchar+3));		//aa
}

指针加1之后,说明内存是往高走的,打印结果也是慢慢往高位打印
这种存放方式叫做小端模式:高位字节存放在高地址,低位字节存放在低地址,
不是所有的系统存放数据都是小端模式,这里做一个了解。
上面的代码在写的时候,遇到一个问题,定义pchar的时候,如果使用 char* 打印结果就是:
在这里插入图片描述
使用unsigned char*定义pchar的时候,结果为:

在这里插入图片描述
原因单独说,printf使用%x占位符打印signed char结果占4个字节?打印unsigned char结果占1个字节?

野指针

野指针指向一个已经删除的对象,或未申请访问受限内存区域的指针,与空指针不同,野指针无法通过简单的判断是否为NULL避免,只能通过良好的编程习惯来尽力减少,

什么情况下会导致野指针?
  • 指针变量未初始化
    任何指针变量刚被创建时不会自动成为NULL指针,他的缺省值是随机的,会乱指一气,所以指针变量在创建的时候应该初始化,
  • 指针释放后未置空
    有时指针在free或delete后,未置为NULL,便会使人以为是合法的,其实free尤其是delete,他们只是把指针所指的内存释放掉,但并没有把指针本身干掉,此时指针指向的就是垃圾内存,释放后的指针应当立即置位NULL,防止野指针的出现
  • 指针操作超越变量作用域
    不要返回指向栈内存的指针或引用,因为栈内存在函数结束时会被释放

指针的步长

看一个例子:

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

int main(void){
    int a  = 100;
    char buf[64] = {0};
    memcpy(buf+1,&a,sizeof(int));	//不存放在第一个字节,从第二个字节开始存
    //取的时候怎么取呢?
    char* p = &buf;
    printf("%d\n",*(int*)(p+1));//指针+1,强转成int类型的指针,再取值
}

所以存的时候,怎么存的,要记住,取得时候要知道从哪里开始取
那么,如果情况复杂一点呢?
如果有一个结构体:

struct Person{
    int a;
    char b;
    char c[64];
    int d;
};

该怎么快速的知道我想要的字段偏移量是多少呢?
有一个函数,需要导一个头文件

#include<stddef.h>
struct Person p = {1,'a',"hello",100};
printf("b offset = %d\n",offsetof(struct Person,b));

函数参数传一个结构体,和一个字段名称,就可以返回字段名称的偏移量,
运行结果:
在这里插入图片描述

指针的间接赋值

通过地址强转为指针再赋值

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

int main(void){
    int a = 10;
    printf("%p\n",&a);//28FF3C
    打印一个int类型的a的地址
    *(int*)0x28FF3C = 100;		int类型的数,地址也是4个字节int类型,前面加上int*他就变成了一个指针,
    							再前面加*取值,再赋值,就可以改变a的值
    printf("%d\n",a);
}

通过修改指针的值

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void changePoint(int** val){	函数使用二级指针接着
    *val = 0x8;					一级指针改值
}
int main(void){
    int* p = NULL;
    printf("%x\n",p);		打印p的地址为0
    changePoint(&p);		将指针再次取地址,升级为二级指针,传进函数
    printf("%x\n",p);		打印地址 结果为8
}

指针做函数参数的输入特性

主调函数分配内存,被调函数使用内存
在堆上分配内存

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void printStr(const char* str){	参数用一个一级指针接着
    printf("%s\n",str);		直接打印即可
}
int main(void){
    char* str = (char*)malloc(sizeof(char)*100);	堆内存开辟空间
    memset(str,0,100);								初始化
    strcpy(str,"nihao shijie");						赋值
    printStr(str);									打印,传入指针变量
}

在栈上分配内存

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void printArray(int* arr,int len){		数组传入参数,会退化为指针,指向第一个元素的位置
    for(int i = 0;i<len;i++){
        printf("%d\n",arr[i]);
    }
}
int main(void){
    int arr[] = {1,2,3,4,5};				栈中定义一个数组
    int len = sizeof(arr)/sizeof(arr[0]);	计算出数组长度
    printArray(arr,len);					将数组和长度传入函数中
}

下面写一个栈内存存放字符串的

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void printStrings(char** strs,int len){	
									我们说,普通类型的数组名当做函数参数,会退化为指针
									上一个例子中,int类型的数组,在函数参数中,要用int*来接着
									所以在这里,char*类型的数组,在函数参数中要用char**来接着,也就是多一层指针,
    for(int i = 0;i<len;i++){
        printf("%s\n",strs[i]);
    }
}
int main(void){
    char* strs[] = {			定义一个char*类型的数组,里面的每一个元素都是char*类型
        "aaaaa",//这些字母都是存储在常量区
        "bbbbb",
        "ccccc",
        "ddddd",
        "eeeee"
        };
    int len = sizeof(strs)/sizeof(strs[0]);	计算长度
    printStrings(strs,len);	传参
}

指针做函数参数的输出特性

被调函数分配内存,主调函数使用内存
代码示例:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
void mallocSpace(char** temp){		因为p是指针,传参的时候又取地址了,所以这里用二级指针接着
    char*p = (char*)malloc(sizeof(char)*100);	重新定义一个指针给分配内存
    memset(p,0,100);							初始化
    strcpy(p,"nihao shijie");					赋值
    *temp = p;									指针重定向
}
int main(void){
    char* p = NULL;				定义一个指针p指向空
    mallocSpace(&p);			把p的地址给了函数,剩下的交给函数
    printf("%s\n",p);			重新打印一下p的内容
    if(p!=NULL){
        free(p);				最后回收这块内存
        p = NULL;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值