C语言
预处理(以#开头)
宏定义
宏可以理解为替换,替换过程不会进行语法检查,语法检查在编译时进行。只替换只替换只替换
1.不带参数的宏定义:
宏定义又称为宏代换、宏替换,简称“宏”。实质为直接替换,宏名一般为大写。
例:
#defined PI 3.1415
2.带参数的宏:
#define 宏名(参数表) 文本
例如:#define S(a,b) a*b
需要注意的是:如果宏替换运算式或者函数一般要加上()
错误使用如:
#defined test 5+3
printf("5倍test = %d", test * 5); //实则计算的是:5+3*5
与函数的区别
带参数的宏和函数很相似,但有本质上的区别:宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。而函数是一段可以重复使用的代码,会被编译,会给它分配内存,每次调用函数,就是执行这块内存中的代码。
3.条件编译
#ifdef
#else
#endif
顾名思义,更急条件判断是否编译,一般方便调试预处理。
4.预定义宏
一般为系统定义宏,用户一般不自己定义,由两根下划线和名字首尾组合,前后都是两根下划线。
如:
__FONCTION__ //函数名
__LINE__ //行号
包含类预处理
形如 #include
后接头文件,实质是把头文件的内容展开编译。
#的其他使用
“#“ 字符串化
”##“ 连接符
#include <stdio.h>
#define ABC(x) #x // #字符串化 “x”
#define DAY(x) day##x //##连接符号
int main()
{
int day1 = 10;
int day2 = 20;
printf(ABC(ab\n) );
/*
printf("the day is %d\n", day1 ); 输出10
*/
printf("the day is %d\n",DAY(1) );
return 0;
}
内核示例:
/* SRAM offsets */
#define ADM8211_SRAM(x) (priv->pdev->revision < ADM8211_REV_BA ? \
ADM8211_SRAM_A_ ## x: ADM8211_SRAM_B_ ## x)
GCC预处理命令
gcc -E hello.c -o hello.i
编译成.i,把需要预处理的代码替换过来。
关键词数据类型
关键字
可以通俗的理解为编译器预先定义的有意义的字符串,不需要用户自己定义编译。
数据类型
说明 | 字符型 | 短整型 | 整型 | 长整型 | 单精度浮点型 | 双精度浮点型 | 无类型 |
---|---|---|---|---|---|---|---|
数据类型 | char | short | int | long | float | double | void |
关键类型讲解
1、bool
bool不是基本的数据类型,是在stdbool.h库中定义的一个数据类型,非常简单只有0和非0,需要注意的是一切不为0的都会被认为1.
2、char
硬件芯片操作的最小单位:bit ,一个bit只有0和1。
在软件代码编写时,一般操作的最小单位是:B,而char a就是一个字节 = 8bit。
可以理解为软件编程时操作硬件的最小单位,。
举个例子:
如果咱们接收数据为字符123,使用int类型的变量来接受4个字节就会浪费资源。使用char buf[x];可以避免此问题。
还有一个原因就是ASCII码表8bit可以表示完全,就定义8bit为一个数据类型。
3、int
系统一个周期,所能接受的最大处理单位(32bit),32bit刚好4B,所以int类型的变量时编译器最优的处理大小;
需要注意的是如果是16位的单片机系统 int 的大小位2B。int的大小是编译器决定的。
需要注意的是再给int型变量赋值时应注意进制问题
如:
int a = 010;
a的值为8;0开头的数字代表八进制;
int a = 0x10;
a的值为16; 0x开头的代表16进制;
4、unsigned、signed
一般使用区别
无符号:用作数据
有符号:用作数字
区别根本:内存空间的最高字节是符号位还是数据位。
C语言规定,在符号位中,用 0 表示正数,用 1 表示负数。例如 int 类型的 -10 和 +16 在内存中的表示如下:
需要注意的是:由于符号位的存在,对于取反操作“~”需多加注意如int a = 1; ~a = -2;
先对正数求补码
然后对补码取反,包括符号位
最后进行一个补码求原码的过程,一定要搞清概念啊。
如果定义char a = -1;则无论如何右移最高位都是1;操作硬件时容易出现错误。在操作数据时不要偷懒省略unsigned,大型项目造成稀奇古怪的问题很难发现错误在哪。
5、float、 double
需要注意的是浮点型数据和整型的数据在内存中表示完全不一样,整型变量和内存中2进制对应,浮点型不对应。
在实际编译时,1.0会被默认为double型占用8个字节,如果小数位数不是那么多可以使用1.0f来代替,表示float。
6、struct
描述元素之间的和,描述比较复杂的数据类型,支持用户自定义数据类型。
需要注意的是:
struct myabc{
unsigned int a;
unsigned int b;
unsigned int c;
unsigned int d;
}
结构体变量里的数据类型顺序有一定的要求,在定义结构体时会根据a、b、c、d的顺序来申请内存。
例如:
#include <stdio.h>
struct myabc
{
char a;
int b;
char c;
};
int main()
{
struct myabc b;
int a = sizeof(b);
printf( "%d\n",a);
return 0;
}
//结果为12;
#include <stdio.h>
struct myabc
{
char a;
char c;
int b;
};
int main()
{
struct myabc b;
int a = sizeof(b);
printf( "%d\n",a);
return 0;
}
//结果为8。
7、union
共用起始地址的一段内存
技巧性代码,改变其中某一个变量其他变量也会改变。
8、enum
枚举类型,占用一个int空间,是一堆整型常数的打包,方便管理。和逐一宏定义没有区别
enum week{
Monday = 0 , Tuesday =1 ,Wednesday = 2,Thursday,Friday,
Saturday,Sunday
};
//需要注意的是以上代码会根据Monday = 0;后面的依次加一,即Thursday = 3,Friday = 4。
9、typedef
作用1:给别人起个外号
int a;
a是一个int类型的变量
typedef int a_t;
a_t是一个int类型的外号
a_t mysize; 就是 int mtsize;
一般格式为xxx_t :的是使用 typedef重新定义的名字。
作用2:获取变量或者表达式的类型
int i;
typedef(i) j = 20; //等于 int 就= 20;
与define的区别
具体区别如下:
- 作用范围不同:
#define
的作用范围是从定义到文件末尾或者遇到另一个相同名称的宏定义为止;而typedef
的作用范围是整个程序。 - 替换方式不同:
#define
是简单地进行文本替换,没有类型检查和语法分析;而typedef
是实现真正意义上的类型重命名,有编译器进行检查和分析。 - 可读性不同:由于 #define 是直接进行文本替换,所以在调试时可能会造成一些问题。而 typedef 则更容易理解和维护
需要注意的是
#define IP int * IP p,q; ----> int * p, q;
#typedef int * IP IP p, q; --> int * P, *q; //给 int * 类型取别名叫IP
一般情况可以利用公式法看typedef
typedef int * FUNCP(int); //把这个看成公式
FUNCP p; -->带入typedef公式把typedef关键字去掉 int * p(int)
GNU C编译器扩展关键字
__attribute__
可以为函数或数据声明赋属性值.给函数分配属性值主要是为了执行优化处理.
使用方式__attribute__((A))
,目前__attribute__
支持十几种属性声明。
- section
- aligend
- packet
- weak
- format
- noinline
- 。。。。。
例如:
struct S {
short b[3];
} __attribute__ ((aligned (8)));
typedef int int32_t __attribute__ ((aligned (8))); //子节对齐
__attribute__ ((packed)) //作用就是告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐,是GCC特有的语法。跨平台时基于数据结构的网络通信经常用
__attribute__ ((section(“section_name”))),其作用是将作用的函数或数据放入指定名为"section_name"对应的段中。
at 绝对定位,可以把变量或函数绝对定位到Flash中,或者定位到RAM。
内联函数
static inline __attribute__((always_inline)) void f() //强制内联
函数调用时会进行保护现象、调用函数、恢复现场、继续执行等操作,开销较大,于是出现内联函数,用于函数短小精悍、调用频繁的函数,内联函数不进行以上开销,直接展开调用,具有宏和函数的两者优点。
内建函数
__builtin开头的函数
就是编译器内部实现的函数
类型修饰符 – 总的来说就是对内存资源存放位置的限定
类型修饰符
auto :默认情况分配内存可读可写的区域,区域在{}中一般认为为栈空间
对分配的内存修饰。
例如: auto int a
**register:**限制变量定义在寄存器中的修饰符,可以定义一些快速访问的变量,如果寄存器不够时a任然会放到存储器中。&对register是无效的。简单的说就是修饰的变量优先放在寄存器中,如果寄存器没有位置,就放在储存器中。
例: register int a。
**static:**静态
应用场景:
1、 修饰函数内部变量(局部)
Int fun()
{
Int a;==》static int a;
}
2、 修饰函数外部变量(全局)
3、 修饰函数
静态全局变量的作用域为定义它的此文件内有效, 在同一源程序的其它源文件中不能使用它。而非静态全局变量的作用域是整个源程序, 当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的,但在别的文件中使用需要使用extern 修饰。
局部变量与局部静态变量不同的是它的存储方式即改变了它的生存期,static局部变量只被初始化一次,下一次依据上一次结果值;同时只要某个对象对静态变量更新一次,所有的对象都能访问更新后的值。
例如:
Int fun()
{
static Int a = 1;
printf(“%d”,a);
a++;
}
此函数输出的a会根据调用的次数增加,不会一直输出a为1.
修饰函数:static函数在内存中只有一份,普通函数在每个被调用中维持一份拷贝
const 常量 只读的变量 (不能直接更改如 const int a = 100; a = 200; 是不允许的,const int *a; 指针指向的内存空间不能被修改,int * const a 指针不能被修改,即指针a = NULL这样的操作不被允许 ),C语言中的const 修饰的变量可以通过一定的手段改变其中的值,但一般不更改,有告诉编译器和其他程序员此变量在此函数中使用并不改变其值方便代码理解和移植。
-
const int *a
表示a
是一个指向常量整型的指针。它可以指向一个整数,指针指向的内存空间不能被修改。也就是说,*a
是只读的,因为它所指向的内存空间被认为是常量。例如:
c复制代码const int *a; int b = 10; a = &b; *a = 20; // 错误:尝试修改只读变量
-
int *const a
表示a
是一个指向整型的常量指针。它指向的变量在声明后不能再改变指向的地址,但可以通过这个指针修改所指向的变量的值。也就是说,a
是只读的,因为它本身被定义成了常量指针,但*a
是可写的。例如:
c复制代码int x = 5, y = 10; int *const a = &x; *a = 20; // 正确:修改所指向的变量的值 a = &y; // 错误:尝试修改常量指针的指向
**extern :**外部声明,外部声明的变量别的文件可以访问。
**volatile :**告知编译器不优化编译 , 易变变量 ,可修饰变量使内存可见,一般操作硬件内存, 一般所修饰变量的值不仅仅可以通过软件更改,也可以通过其他方式更改。
例如:
int a = 100;
while( a==100 ) ;
在编译器编译的过程中第一次给a赋值后a为100,此后while循环没有再给a赋值,编译器会自动优化认为a就是100,如果外部条件改变a,编译器将不会跳出循环。使用volatile修饰可以让编译器不优化,每次都会检查a的值再去和100比较。
逻辑运算符
**||:**A||B 与B||A 不一样,有先后顺序,A||B若A为真,B将不执行
**&&:**A&&B 与B&&A不一样,A&&B若A为假,B将不执行。
以上A和B不一定是代表数字,也可以代表函数,注意不执行的情况。
**! 😗*逻辑取反,如:int A = 0x00; !a为真,除0之外的其他逻辑取反全为0;
**~ 😗*为逐位取反 按照2进制每一位都取反
位运算(详见二进制总结章节)
!!!!!!!对于有符号的数需要注意(和编译器有关)
**<< 左移 :**左移一位相当于乘以2,二进制下的移位
m<<n :相当于m*2^n
**>> 右移:**和符号变量相关,若变量有符号,正数 右移一位补0,负数最高位为1,其他为补0.
M>>n :相当于m/2^n
位运算如何实现加减乘除(「ghscarecrow」的摘取文章)
https://blog.csdn.net/ghscarecrow/article/details/79944305
关注二进制的加法,我们容易发现这样的一个规律:
(1)位运算异或与求和运算结果一致
异或逻辑运算:11=0,10=1,01=1,00=0
求和算术运算:1+1=0,1+0=1,0+1=1,0+0=0
(2)位运算与逻辑运算与求“进位”的结果一致。
与逻辑运算:1&1=1,1&0=0,0&1=0,0&0=0
求进位运算:1+1=1,1+0=0,0+1=0,0+0=0
1.位运算实现加法运算:设置一个变量存储被加数a和b的“和”,然后对a与b进行与运算并左移一位,再将temp值赋值与a,重复上述操作直至b为0时返回a,此时a即为目标值。
2.位运算实现减法运算:由于a-b等价于a+(-b),所以我们可以先将b转换为补码形式,然后调用加法函数便可实现减法运算。
3.位运算实现乘法运算:我们知道,a*b其实就是等价于b个a相加,所以,我们可以利用这个特点,对a进行累加b次,便可以实现乘法运算。
4.位运算实现除法运算:为了实现除法运算,我们可以设置一个累加器result,当result与除数b的乘积大于被除数a时跳出循环,否则result+1后继续比较。当跳出了循环,即意味着者此时的result-1则为目标值。
/*
位运算实现加减乘除
*/
#include <stdio.h>
//加法函数
int add(int a,int b)
{
int temp;
while(b)//当b为0时则代表没有进位了
{
temp=a^b;
b=(a&b)<<1;
a=temp;
}
return a;
}
//减法函数
int sub(int a,int b)
{
b=~b+1;//将b转换为其补码形式
return add(a,b);
}
//乘法函数
int mul(int a,int b)
{
int sum=0;
while(b)
{
sum=add(sum,a);
b--;
}
return sum;
}
//除法函数
int division(int a,int b)
{
int result=0;
if(a==0)
{
return 0;
}
while(1)
{
if(a>mul(a,result))
{
break;
}
result++;
}
return result-1;
}
int main()
{
int a,b,c;
scanf("%d%d",&a,&b);
//测试加法函数
c=add(a,b);
printf("%d\n",c);
//测试减法函数
c=sub(a,b);
printf("%d\n",c);
//测试乘法函数
c=mul(a,b);
printf("%d\n",c);
//测试除法函数
c=division(a,b);
printf("%d\n",c);
return 0;
}
&: 屏蔽和取出 A&0 = 0;
例: int a = 0x1234;a&0xff00; 屏蔽低八位取出高八位
清除第五位,其他位不变 a & ~(0x1<<5);
|: 保留 A|1 = 1;
例:设置第五位为高电平,其他位不变 a| (0x1<<5);
^: 逐位操作,相同为0,不同为1,1^1 = 0; 1^0 = 1;
例:在不引入第三个变量时的换位赋值操作:
a = 1; b = 2;
a = a^b;
b=a^b;
a = a^b;
结果:a = 2; b = 1;
位运算都是2进制下逐位操作,不要带入十进制计算。
指针数组空间
指针
指针变量通俗的理解就是存放地址的盒子,也就是说指针指向内存空间的地址,但要注意的是指针指向内存空间的低地址。
使用一般原则:
C语言使用指针要有有2个疑问?
1、分配一个盒子,盒子要多大?
在32bit系统中。指针就4个字节
2、盒子里存放的地址所指向内存的读取方法是什么?
是int 还是char等等;
另外还要注意的有:
指针读取内存尽量使用无符号指针 ;
操作指针时一定要确定指针指向的类型和指针类型相同;
指针指向的地址一定要保证合法存在;
指针指向字符串不一定有‘\0’结尾,逐一拷贝一定要注意,结束在哪里?一般定义个数拷贝三要素:
源 目的地 拷贝个数
一般使用方法:
Int *p;
P = &a;指针p指向a的地址
Printf(“%d”,*p);取出a的值
指针修饰符
const char *p; 一般指向字符串“hello world”等 表示指针指向的内容是按照一个字节的方式去读,指向的数据(*p
)为只读数据,指向的地址可以变,内容不可改变和 char const *p 相同;
char * const p; 指向的地址不可以变,内容可以变 一般用于硬件资源
const char * const p 指向的地址不可以是ROM
volatile char p ;防止优化指向内存地址
指针基本运算
+:如果p = 0x12;那么p+1 为[0x12 + 1(sizeof(*p))]
**- **:与++相反
指针逻辑运算
“==”
“!=”
跟一个特殊值进行比较,其中0x0表示地址的无效值,结束标志,一般来说指针必须是同类型的比较才有意义
如:
if( p == 0x0)
NULL
多级指针
存放地址的地址空间(和一级指针类似)
指针数组、数组指针
int (*p)[5] 定义一个指针p 读内存的方式为 5个int的读 ,也可以理解为指针p指向一个包含五个元素的数组,指针类型为int
int *p[5] 定义数组p,包含五个元素,数组p为int * 类型的,即每个元素都是一个指针
读取原则:有(),先读(),没(),先右后左,每遇到()就改变方向
int *(*(*f)(int)) [10]
先括号里的*f,是一个指针,往右看(int),说明是一个函数指针,碰到()往左看*,返回的是一个指针,又碰到(),往右看,数组,说明是一个指针数组,往左int型的指针数组,即是一个函数指针,参数为int 返回值为一个指向指针数组的指针
数组
数组空间赋值一般按照数组标签逐一赋值处理,但如果像变量一样赋值,大的数组空间工作量过大,可以只赋值要初始化的,
如:
int a[10] = {1,2,3};
//此时a[0] = 1 a[1] = 2 a[2] = 3 其他的会被置为0;
字符数组:
char buf[10] = {'a','b','c'};
不同的是buf当成一个字符串来看,最后会加上一个’\0’或者0,字符串的重要属性,结尾一定有个’\0’;
也可以这样初始化:char buf[10] = {"abc“};
//C语言编译器会在结尾自动加‘\0’,但是不可以直接buf = “123”;这样初始化。
需要注意的是:
指针指向字符串不一定有‘\0’结尾,操作字符串时一定要注意,结束在哪里?弄清楚字符串操作三要素:
1、src 源
2、dest 目的地
3、个数
a[10]. a代表数组的首地址,&a代表整个数组的地址 *如果a = 0x12;那么a+1 为[0x12 + 1(sizeof(p))] &a+1 =[0x12 + 10(sizeof(*p))] **
当同一个数组的两个成员的指针相减时,其差值为:地址值的差,再除以-个数组成员的size。这个结果代表
了两个指针对应元素的下标之差
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mMMEMEuD-1686838616050)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20230605172731791.png)]
指针与数组的联系
相同点:
指针数组都可以通过*来访问,也都可以通过[ ]来访问
数组做参数时相当于指针 f(int a[10]) 等价于f(int *p)和f(int a[])
区别:
指针有自己的地址
sizeof()检测指针的大小是检测指针变量的大小,不是指针指向变量的大小 与数组不同
指针需要间接访问,数组可以直接访问
指针主要用于非连续地址,数组是连续的
数组的地址就是变量名
例如:
int main()
{
int a[10] = { 1 };
int b = 0;
int *p = b;
printf("&p:%p\n",&p);
printf("&b:%p\n", &b);
printf("a : %p\n", a);
printf("a+10 : %p\n", a+10);
printf("&a+1 : %p\n", &a+1);
}
&p:00EFF938
&b:00EFF944
a : 00EFF950
a+10 : 00EFF978
&a+1 : 00EFF978
空间分布
可以用一个图来概括:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3U2lIPZA-1686838616051)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20230605193805363.png)]
需要注意的是:
1、各个空间变量的生存时间
2、代码段和常数段都是只读段如果更改会出现段错误
3、定义变量要注意:大小 默认方式 在那放
4、堆空间:运行时才有,运行时可以自由分配和释放的空间,生存时间由程序员决定分配;
5、 malloc(),一旦成功,返回分配好的地址,只需要接收,对于这个地址的读法,由程序员决定,输入参数指定分配的大小,单位就是B。申请完用完记得释放,不然会造成内存泄漏,有申请不成功的可能一般申请方式如下
char *p;
p = (char *)malloc(100);
if(p == NULL){
error
}
6、只读空间:静态空间,程序结束时释放
7、栈空间: 函数内部使用的变量,函数一旦结束就释放
字节对齐
舍弃部分内存来提高运行速度的一种方式,一般为8字节对齐,举两个具体操作的示例代码:
低八位不为0,字节没有对齐,加7,造成高八位加1,使低八位置0,字节对齐
portBYTE_ALIGNMENT_MASK 7
portBYTE_ALIGNMENT 8
if ( ( uxAddress & portBYTE_ALIGNMENT_MASK ) != 0 ) {
uxAddress += ( portBYTE_ALIGNMENT - 1 );
uxAddress &= ~( ( size_t ) portBYTE_ALIGNMENT_MASK );
xTotalHeapSize -= uxAddress - ( size_t ) ucHeap;
/* 需要申请的内存大小与系统要求对齐的字节数不匹配,需要进行内存对齐
查看相比对齐还差几位,在空间后面加上对齐*/
if ( ( xWantedSize & portBYTE_ALIGNMENT_MASK ) != 0x00 ) {
xWantedSize += ( portBYTE_ALIGNMENT - ( xWantedSize &
portBYTE_ALIGNMENT_MASK ) );
}
需要注意的就是在一些定义中和定义变量顺序有关
struct abc
{
char a;
short e;
int b;
}
struct def
{
short e;
int b;
char a;
}
两结构体占空间不同
函数
通俗理解就是一堆代码的集合,用一个标签来描述它,用于需要的地方复用。一般具有承上启下的作用,输入一些东西操作之后再输出一些东西。
函数一般包括三要素:1、函数名(函数地址标签)2、输入参数 3、返回值
由于函数名为一块特殊地址的标签,则可以用指针描述函数
int (*p)(int, int, char);//右边()告诉指针P是函数类型的指针,输出参数为int int char 类型的,左边告诉指针P返回值为int类型的。以例子可以实现printf的功能。
int (*myshow)(const char *,......);
myshow = printf;
myshow("12345\n");
函数的参数的传递
函数的调用一般有调用者和被调者。
调用者(使用函数的地方)
方式:
函数名(传递的数据) //要传递的数据为实参
被调者(函数实现的地方)
方式:
函数返回值类型 函数名(要接受的数据) //接收的数据为形参
{
函数代码具体实现XXXX
xXXXXX
}
实参传递给形参传递的形式:按位拷贝赋值 以下例子结果为34
void myswap(char buf)
{
printf("the buf is %x\n", buf);
}
int main()
{
myswap(0x1234);//0x1234为实际参数,会拷贝一份 0x34为形式参数
return 0;
}
由于实参传递给形参是拷贝的方式,所以直接传值不会改变实参的大小,可以理解为我传递的参数是原来的备份不会对我原来的空间的值照成影响,但使用地址传递可以改变别的空间的变量值,像结构体、数组这样大的连续空间的传递一般为地址传递,减少内存占用
在函数实现的时候为了传递地址提示程序员不能修改,一般加上const修饰。
void char(const char *p) //p指向的空间值不能修改
*对于字符空间地操作一般注意是否能修改,即是否需要要const修饰,若用const修饰一般使用“xxx”来初始化,char p 一般使用buf来初始化,结束一般判断是否为‘\0’
对于非字符空间一般传递连续空间时,一般需要再传首地址的同时传空间的大小
void(char *p)//操作非字符空间不规范
void(char *p, int len)//较为规范
一般地址传递参数为void*代表对地址的修改,而传递指定类型的如:int *
代表对值的修改
到这函数的参数传递告一段落了,可以看一下下面这个例子检测是否理解
以下例子P不会改变:
void f( char *p)
{
*p = '2';
}
int main()
{
char *p;
char a = '1';
p = &a;
f(p);
printf("%c\n",*p);
return 0;
}
形参拷贝P的值(p指向的一个地址),无论对p指向的地址的副本怎么操作,p指向的地址和p本身的地址都不会改变,但P指向的地址的值可能被改变。
函数的返回值
返回值和参数传递类似,也是逐一拷贝的原理,可以接收值也可以接收地址。
需要注意的是:
1、以下函数是返回指针的地址:
int *fun(void); p = fun();
int fun(int **p); fun(&p);
2、在C语言函数中不能对数组的空间,只能通过指针返回数组的首地址,
3、指针是空间返回的唯一数据类型
4、地址:指向的合法性
作为函数的设计者,必须保证函数返回的地址所指向的空间是合法。不是局部变量。
三个函数对比理解:
char *fun()
{
char buf[] = "hello";
return buf; //buf,会在返回之后 返回之后被回收,等到buf的地址是一个栈空间,不能等到hello
}
char *fun()
{
char *p = (char *)malloc(100);
strcpy(p,"hello");
return p; //返回的是p指向的堆空间,整个程序结束时或者被主动释放时才被释放
}
char *fun()
{
static char buf[] = "hello";
return buf;//返回静态空间地址,在本文件中,直到程序结束才会被释放。
}
二进制总结
二进制中的原码、反码、补码
有符号数: 对于有符号数而言,符号的正、负机器是无法识别的,但由于“正、负” 恰好是两种截然不同的状态,如果用“0”表示“正”,用“1”表示“负”,这样 符号也被数字化了,并且规定将它放在有效数字的前面,即组成了有符 号数。所以,在二进制中使用最高位(第一位)来表示符号,最高位是 0,表示正数;最高位是1,表示负数。
无符号数: 无符号数是针对二进制来讲的,无符号数的表数范围是非负数。全部二 进制均代表数值(所有位都用于表示数的大小),没有符号位。即第一 个"0"或"1"不表示正负
对于有符号数而言的性质:
(1)二进制的最高位是符号位:0表示正数,1表示负数
(2)正数的原码、反码、补码都一样
(3)负数的反码 = 它的原码符号位不变,其他位取反(0 ->1 ; 1->0 )
(4)负数的补码 = 它的反码 +1
(5)0的反码、补码都是0
(6)在计算机运算的时候,都是以补码的方式来运算的
有符号数运算案例
1. 正数相加: 例如:1+1 ,
在计算机中运算如下:
1的原码为: 00000000 00000000 00000000 00000001
反码: 00000000 00000000 00000000 00000001
补码: 00000000 00000000 00000000 00000001
两数的补码相加: 00000000 00000000 00000000 00000010( 转换为10进制) = 2
-
正数相减: 例如:1 - 2,
在计算机中运算如下:
在计算机中减运算其实是作为加运算来操作的,所以,1-2 = 1 + ( -2 )
第一步:获取1的补码 00000000 00000000 00000000 00000001
第二步:获取-2的补码 -2的原码:10000000 00000000 00000000 00000010
-2的反码:11111111 11111111 11111111 11111101
-2的补码: 11111111 11111111 11111111 11111110
第三步:1的补码与-2的补码相加: 00000000 00000000 00000000 00000001 + 11111111 11111111 11111111 11111110 = 11111111 11111111 11111111 11111111
第四步:将计算结果的补码转换为原码,反其道而行之即可(如果想将 二进制转换为十进制,必须得到二进制的原码)
补码:11111111 11111111 11111111 11111111
反码:11111111 11111111 11111111 11111110
原码:10000000 00000000 00000000 00000001
第五步:将计算结果的二进制原码 转换 为十进制 二进制原码:10000000 00000000 00000000 00000001 = -1
-
取反
取反一样会涉及到符号位:
1的源码: 01
~取反(不是反码):11111111 11111111 11111111 11111110
转换为十进制,系统会认为11111111 11111111 11111111 11111110 为补码,
变源码:补码-1,取反:11111111 11111111 11111111 11111101 -》 10000000 00000000 00000000 00000010 = -2
<< 、>>位移运算符
<< 左移运算符
左移一位后的数值经过计算可以发现刚好值位移前数值的两倍,等价 于乘2操作,在很多情况下可以当做乘2使用,但是并不代表真正的乘2,在 一些特殊情况下并不等价,当触及到符号位由于左移变化时,不相等,左移18位:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eRyxgn4L-1686838616056)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\image-20230607104829397.png)]
此时二进制首位为1,此时数值为 -1058799616,同理,如果左位移 20位,则值为 59768832 又变成了正数
注意:所以根据这个规则,如果任意一个十进制的数左位移32位,右 边补位32个0,十进制岂不是都是0了?当然不是!!! 当int 类型的数 据进行左移的时候,当左移的位数大于等于32位的时候,位数会先求 余数,然后用该余数进行左移,也就是说,如果真的左移32位的时 候,会先进行位数求余数,即为左移32位相当于左移0位 ,所以左移 33 的值和左移一位1 是一样的
>> 右移运算符
对于无符号数右移,等价 于除2操作,最高位补0,对于有符号数和编译器有关,为避免编译器差异,可以进行如下操作:
int x = -10; // x为带符号整数,初始值为-10
unsigned int ux = (unsigned int) x; // 将x转换为无符号数
ux = ux >> 1; // 右移一位
x = (int) ux; // 将无符号数转换为带符号整数
若不行对符号位操作可以先确定符号位,再操作
int flag = 1;
if(x < 0)
{
flag = -1;
x = -x;
}
x = x>>1;
x = flag*x;
11111111 11111111 11111110
转换为十进制,系统会认为11111111 11111111 11111111 11111110 为补码,
变源码:补码-1,取反:11111111 11111111 11111111 11111101 -》 10000000 00000000 00000000 00000010 = -2
<< 、>>位移运算符
<< 左移运算符
左移一位后的数值经过计算可以发现刚好值位移前数值的两倍,等价 于乘2操作,在很多情况下可以当做乘2使用,但是并不代表真正的乘2,在 一些特殊情况下并不等价,当触及到符号位由于左移变化时,不相等,左移18位:
[外链图片转存中…(img-eRyxgn4L-1686838616056)]
此时二进制首位为1,此时数值为 -1058799616,同理,如果左位移 20位,则值为 59768832 又变成了正数
注意:所以根据这个规则,如果任意一个十进制的数左位移32位,右 边补位32个0,十进制岂不是都是0了?当然不是!!! 当int 类型的数 据进行左移的时候,当左移的位数大于等于32位的时候,位数会先求 余数,然后用该余数进行左移,也就是说,如果真的左移32位的时 候,会先进行位数求余数,即为左移32位相当于左移0位 ,所以左移 33 的值和左移一位1 是一样的
>> 右移运算符
对于无符号数右移,等价 于除2操作,最高位补0,对于有符号数和编译器有关,为避免编译器差异,可以进行如下操作:
int x = -10; // x为带符号整数,初始值为-10
unsigned int ux = (unsigned int) x; // 将x转换为无符号数
ux = ux >> 1; // 右移一位
x = (int) ux; // 将无符号数转换为带符号整数
若不行对符号位操作可以先确定符号位,再操作
int flag = 1;
if(x < 0)
{
flag = -1;
x = -x;
}
x = x>>1;
x = flag*x;