零碎知识点
函数定义不可以嵌套
函数调用可以嵌套
数组不能相互赋值
以返回值和形参为标准
运算符优先级
编译流程
主要分为4个步骤:
① 预处理(预编译):宏定义展开,头文件展开,条件编译,这里并不会检查语法在编译代码基本语法之前,进 行头文件的引入和宏定义替换 ---- gcc -E test.c -o test.i
注意:因为我们知道预编译步骤根本就没有帮你编译普通的主要代码。
② 编译(检查C语言语法的过程,如果语法没有错,则会生成汇编文件) ---- gcc -S test.i -o test.s
汇编语言是人类控制硬件最接近程序设计代码
机器码是直接控制及硬件的代码。
③ 汇编(检查汇编语法的过程) ---- gcc -c test.s -o test.o
检查.s文件中的汇编代码的语法有没有错,如果没错就把汇编代码编译生成机器码,但是这个机器码不能运行
④ 链接:将目标文件链接成可执行程序(二进制文件) 后缀名.o test.o:二进制文件 (缺库,一般都缺少C 标库),是不能运行的,需要先链接库之后,才能生成一 个真正能运行的程序 ----- gcc test.o -o test
gcc 在编译时默认链接的库可以分为两类:系统默认库和标准库。
- 系统默认库:这些库包括系统提供的常用库,例如 libc、libm 等。它们通常会被默认链接到你的程序中,以提供基本的系统功能和标准的数学函数等。在 Linux 系统中,通常使用
gcc
编译时会默认链接 libc 库。 - 标准库:这些库包括 C 标准库(libc)、C++ 标准库(libstdc++)等。在编译 C 程序时,默认会链接 C 标准库,而编译 C++ 程序时,默认会链接 C++ 标准库。这些库提供了一系列标准函数和类,用于处理字符串、文件、内存分配等任务。
除了默认链接的库之外,你还可以通过命令行参数来手动链接其他的库,例如 -lm
表示链接数学库,-lpthread
表示链接 POSIX 线程库等。
如果你希望查看 gcc
默认链接的库,可以使用以下命令:
gcc -v -o output_file input_file.c
这会显示 gcc
在编译时实际调用的命令,其中会包含默认链接的库
gcc a -lc -lgcc
这个命令使用了 gcc 编译器来编译文件 a
,并手动链接了两个库:libc
和 libgcc
。
-lc
:这个选项告诉编译器手动链接 C 标准库(libc),这样你的程序就可以使用标准的 C 函数和特性。-lgcc
:这个选项手动链接了 libgcc 库,它包含了一些与 GCC 相关的运行时库函数,例如处理整数溢出、除法等。这个库在一些特殊情况下可能被使用,但一般情况下不需要手动链接,因为 GCC 会默认链接这个库。
所以,这个命令相当于告诉编译器在编译 a
文件时需要链接 C 标准库和 libgcc 库。
ldd工具 (Linux库的后缀: so 和 a windows:dll ) //查看程序需要链接的动态库
如
linux:ldd a.out
C语言内存分区
程序运行前:
代码区:
共享
只读
数据区:
静态变量,全局变量,常量
已初始化的:在data区
未初始化的:在bss区
全局(静态)存储区:
分为 DATA 段和 BSS 段。DATA 段(全局初始化区)存放初始化的全局变量和静态变量;
BSS 段(全局未初始化区)存放未初始化的全局变量和静态变量。程序运行结束时自动释放。其中 BBS段在程序执行之前会被系统自动清0,
所以未初始化的全局变量和静态变量在程序执行之前已经为0。存储在静态数据区的变量会在程序刚 开始运行时就完成初始化,也是唯一的一次初始化。
程序运行时:
栈区(stack):
先进后出,容量有限会栈溢出(比如死循环)
由编译器自动分配释放
存放函数的参数值,返回值,局部变量等。
在程序运行过程中实时加载和释放,因此。局部变量的生存周期为申请到释放该段栈空间
堆区(heap):
堆区的容量远远大于栈区,先进先出
一般是程序员动态内存分配和释放,若程序员不释放,程序结束时由操作系统回收 用malloc和free 申请和释放
堆在内存中位于BSS区和栈区之间
栈区
栈区介绍:
栈区由编译器自动分配释放,由操作系统自动管理,无须手动管理。
栈区上的内容只在函数范围内存在,当函数运行结束,这些内容也会自动被销毁。
栈区按内存地址由高到低方向生长,栈底是高地址,栈底是低地址,向下生长.其最大大小由编译时确定,速度 快,但自由性差,最大空间不大。
栈区是先进后出原则,即先进去的被堵在屋里的最里面,后进去的在门口,释放的时候门口的先出去。
存放内容
临时创建的局部变量和const定义的局部变量存放在栈区。
函数调用和返回时,其入口参数和返回值存放在栈区。
堆区
堆区介绍
堆区由程序员分配内存和释放。
堆区按内存地址由低到高方向生长,其大小由系统内存/虚拟内存上限决定,速度较慢,但自由性大,可用空间大。
调用函数
用malloc等函数实现动态分布内存。
void malloc(size_t);
参数size_t是分配的字节大小。
返回值是一个void型的指针,该指针指向分配空间的首地址。
(void 型指针可以任意转换为其他类型的指针)
用free函数进行内存释放,否则会造成内存泄漏。
void free(void * /ptr*/);
参数是开辟的内存的首地址。
全局(静态)区
一般来说,在一起定义的两个全局变量,在内存的中位置是相邻的。这是一个简单的常识,但有时挺有用,如果一个全局变量被破坏了,
不防先查查其前后相关变量的访问代码,看看是否存在越界访问的可能。
全局(静态)区介绍
通常是用于那些在编译期间就能确定存储大小的变量的存储区,但它用于的是在整个程序运行期间都可见的全局变量和静态变量。
全局区有 .bss段 和 .data段组成,可读可写。
.bss段(Block Storage Start)
通俗的说,bss是指那些没有初始化的和初始化为0的全局\静态变量。
它的特点如下:
1,bss类型的全局变量只占运行时的内存空间,而不占文件空间。
int bss_array[1024 * 1024] = {0};
int main(int argc, char* argv[])
{
return 0;
}
[root@localhost bss]# gcc -g bss.c -o bss.exe
[root@localhost bss]# ll
total 12
-rw-r--r-- 1 root root 84 Jun 22 14:32 bss.c
-rwxr-xr-x 1 root root 5683 Jun 22 14:32 bss.exe
//变量bss_array的大小为4M,而可执行文件的大小只有5K。
2,大多数操作系统,在加载程序时,会把所有的bss全局变量全部清零,无需要你手工去清零。
但为保证程序的可移植性,手工把这些变量初始化为0也是一个好习惯。
3,未初始化的全局变量和未初始化的静态变量存放在.bss段。
初始化为0的全局变量和初始化为0的静态变量存放在.bss段。
.bss段不占用可执行文件空间,其内容由操作系统初始化。
4,用static修饰的局部变量它的作用域是局部的,但生命期是全局的。
在有的嵌入式平台中,堆实际上就是一个全局变量,它占用相当大的一块内存,在运行时,把这块内存进行二次分配。
.data段
已初始化的全局\静态变量存放在.data段。
已初始化的静态变量存放在.data段。
.data段占用可执行文件空间,其内容有程序初始化。
data指那些初始化过(非零)的非const的全局变量。
它有什么特点呢,我们还是来看看一个小程序的表现。
int data_array[1024 * 1024] = {1};
int main(int argc, char* argv[])
{
return 0;
}
[root@localhost data]# gcc -g data.c -o data.exe
[root@localhost data]# ll
total 4112
-rw-r--r-- 1 root root 85 Jun 22 14:35 data.c
-rwxr-xr-x 1 root root 4200025 Jun 22 14:35 data.exe
//仅仅是把初始化的值改为非零了,文件就变为4M多。 由此可见,data类型的全局变量是即占文件空间,又占用运行时内存空间的。
常量区rodata (only read)
字符串、数字等常量存放在常量区。
const修饰的全局变量存放在常量区。
程序运行期间,常量区的内容不可以被修改。
rodata 的意义同样明显,ro代表read only,即只读数据(const)。关于 rodata 类型的数据,要注意以下几点:
常量不一定就放在 rodata 里,有的立即数直接编码在指令里,存放在代码段(.text)中。
对于字符串常量,编译器会自动去掉重复的字符串,保证一个字符串在一个可执行文件(EXE/SO)中只存在一份拷贝。
int main()
{
char *str1 = "hello";
char *str2 = "hello";
printf("%p------%p\n",str1,str2); //地址一样
return 0;
}
rodata 是在多个进程间是共享的,这可以提高空间利用率。
在有的嵌入式系统中, rodata 放在ROM(如norflash)里,运行时直接读取ROM内存,无需要加载到RAM内存中。
在嵌入式linux系统中,通过一种叫作XIP(就地执行)的技术,也可以直接读取,而无需要加载到RAM内存中。
由此可见,把在运行过程中不会改变的数据设为 rodata 类型的,是有很多好处的:在多个进程间共享,可以大大提高空间利用率,甚至不占用RAM空间。
同时由于 rodata 在只读的内存页面(page)中,是受保护的,任何试图对它的修改都会被及时发现,这可以帮助提高程序的稳定性。
代码区
程序执行代码存放在代码区,其值不能修改(若修改则会出现错误)。
字符串常量和define定义的常量也有可能存放在代码区。
静态变量,全局变量,局部变量,常量
局部变量
在一个函数内部定义的变量是内部变量,它只在本函数范围内有效,也就是说只有在本函数内才能使用它们,在此函数以外时不能使用这些变量的,它们称为局部变量。
局部变量保存在动态数据区的栈中,只有在所在函数被调用时才动态地为变量分配存储单元。
1).主函数main中定义的变量也只在主函数中有效.
2).不同函数中可以使用名字相同的变量,它们代表不同的对象,互不干扰.
3).形参也是局部变量.
4).在复合语句中定义的局部变量,这些变量只在本复合语句中有效.
全局变量
全局变量:
再全局位置定义的变量就是全局变量,在其他文件中使用的时候需要声明,声明用关键字extern
属于外部链接属性(在一个文件夹下的不同文件中可以相互调用,不过需要提前声明,声明不是定义,只是调用)
eg:
b 在其他文件中全局位置已经定义;不是局部位置,局部变量是引用不了的
extren int b; //告诉编译器其他文件中有一个b,链接的时候在其他文件中寻找,找不到会报错(其他文 件中没有就会报错,不会在自己这边找)
但是如果是自己这边全局作用域的位置定义extern int b = 10;是不会报错的,相当于在自己这边定义 了,引用的时候不需要包含头文件
各种情况如下:
fun()
{
extern int b; //在函数中声明会去其他文件寻找,如果其他文件没有会报错,在函数内使用只能声明,不能初始化 即:extern int b = 10;错误
b = 1;
}
带extern全局变量只能在全局位置初始化,在局部只能声明;在全局初始化的前提是其他文件中没有该变量(extern int a = 10;其他文件没有全局变量a)
其他文件没有定义全局变量b的情况下:
int main()
{
extern int b;
printf("%d",b);
return 0;
}
//执行的时候会报错:无法解析的外部命令:这个报错一般是链接阶段的错误。但是不使用b只是声明一下就不会报错,如下
int main()
{
extern int b;
return 0; //不报错
}
全局变量可以为本文件中其它函数所共用,它的有效范围从定义变量的位置开始到本源文件结束。
全局变量位于静态全局数据区中。
1).设全局变量的作用:增加了函数间数据联系的渠道.
2).建议不再必要的时候不要使用全局变量,因为
a.全局变量在程序的全部执行过程中都占用存储单元.
b.它使函数的通用性降低了
3).如果外部变量在文件开头定义,则在整个文件范围内都可以使用该外部变量,如果在定义点之前的函数想引用该外部变量,则应该在该函数中用关键字extern作外部变量说明.
4).如果在同一个源文件中,外部变量与局部变量同名,则在局部变量的作用范围内,外部变量不起作用.
!全局位置不加extern默认就有extern修饰
静态变量
静态变量并不是说其就不能改变值,不能改变值的量叫常量。 其拥有的值是可变的 ,而且它会保持最新的值。说其静态,是因为它不会随着函数的调用和退出而发生变化。即static局部变量只被初始化一次,下一次依据上一次结果值;
静态变量的作用范围要看静态变量的位置,如果在函数里,则作用范围就是这个函数。可以将其返回出去因为其生命周期长不会随着函数的退出而释放
静态变量属于静态存储方式,其存储空间为内存中的静态数据区(在静态存储区内分配存储单元),该区域中的数据在整个程序的运行期间一直占用这些存储空间(在程序整个运行期间都不释放),也可以认为是其内存地址不变,直到整个程序运行结束(相反,而auto自动变量,即动态局部变量,属于动态存储类别,占动态存储空间,函数调用结束后即释放)。静态变量虽在程序的整个执 行过程中始终存在,但是在它作用域之外不能使用。可以通过返回静态变量的指针在外部使用。
另外,属于静态存储方式的量不一定就是静态变量。 例如:全局变量虽属于静态存储方式,但不一定是静态变量,必须由 static加以定义后才能成为静态全局变量,或称静态外部变量。
全局变量(外部变量)的说明之前再冠以static 就构成了静态的全局变量。全局变量本身就是静态存储方式, 静态全局变量当然也是静态存储方式.这两者在存储方式上并无不同。这两者的区别虽在于非静态全局变量的作用域是整个源程序,当一个源程序由多个源文件组成时,非静态的全局变量在各个源文件中都是有效的.而静态全局变量则限制了其作用域, 即只在定义该变量的源文件内有效, 在同一源程序的其它源文件中不能使用它.由于静态全局变量的作用域局限于一个源文件内,只能为该源文件内的函数公用,因此可以避免在其它源文件中引起错误.
从以上分析可以看出, 把局部变量改变为静态变量后是改变了它的存储方式即改变了它的生存期。把全局变量改变为静态变量后是改变了它的作用域, 限制了它的使用范围.
static也能修饰函数,默认函数是全局的
在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
静态变量:(有全局静态变量,局部静态变量)
程序运行前分配内存
生命周期在程序运行结束死亡
默认属于内部链接属性,在当前文件中使用,(就是项目文件夹中有很多.c文件只有一个有主函数main)
在同一个文件夹下的一个文件中定义的静态变量只能在这个文件中使用不能被其他文件调用,其他 文件若想调用需要导入头文件,导入头文件是可以使用的)
static int a = 10;
可以节省内存,因为它是所有对象所公有的,因此,对多个对象来说,静态数据成员只存储一处,供所有对象共用。静态数据成员的值对每个对象都是一样,但它的值是可以更新的。
只要对静态数据成员的值更新一次,保证所有对象存取更新后的相同的值,这样可以提高时间效率。
总的来说
(1)在修饰变量的时候,static 修饰的静态局部变量只执行初始化一次,而且延长了局部变量的生命周期,直到程序运行结束以后才释放。
(2)static 修饰全局变量的时候,这个全局变量只能在本文件中访问,不能在其它文件中访问,即便是 extern 外部声明也不可以。(.h文件中有静态变量导入此头文件是可以使用静态变量的)
(3)static 修饰一个函数,则这个函数的只能在本文件中调用,不能被其他文件调用。static 修饰的变量存放在全局数据区的静态变量区,
包括全局静态变量和局部静态变量,都在全局数据区分配内存。初始化的时候自动初始化为 0。
(4)不想被释放的时候,可以使用static修饰。比如修饰函数中存放在栈空间的数组。如果不想让这个数组在函数调用结束释放可以使用 static 修饰。
(5)考虑到数据安全性(当程序想要使用全局变量的时候应该先考虑使用 static)
C语言中:
静态变量的初始化只能使用常量表达式。
int b = 10;
static int *a = b ; 错误
没有初始化的静态变量(全局变量)放在数据段里面的.bss
static int a;
初始化的静态变量(全局变量)放在数据段里面的.data
static int a = 11;
常量
const修饰的常量(有全局常量和局部常量)
const修饰的全局变量:
直接修改:失败
间接(通过指针)修改:语法通过 运行失败,放在常量区中,受常量区的保护:只读
const修饰的局部变量(伪常量):
直接的方式无法修改,间接的方式可以修改
const修饰的局部变量放在栈区中
伪常量不能初始化数组用:char c[b] //b局部常量(伪常量)
字符串常量:(双引号包起来的)
字符串常量是共享的
char * p1 = "helloworld";
char * p2 = "helloworld";
char * p3 = "helloworld";
printf("%d",p1);
printf("%d",p2); //vs将相同的字符串常量看成一个,
printf("%d",p3); //四者输出的值相同,指向的都是同一个,有些编译器也不是同一个
printf("%d",&"helloworld");
字符串常量不一定不能修改的,ANSI规定:修改字符串常量,结果是未定义的
char * p1 = "helloworld";
p1[0] = 'c'; //修改字符串常量,可能可以,可能不可以
char *p = 'a';
在C语言中,你声明了一个字符指针 p,但是赋值的方式是不正确的。单引号 'a' 表示一个字符常量,而不是一个字符串。如果你想将一个字符赋给指针变量,你可以使用双引号 "a" 来表示一个包含该字符的字符串。正确的代码应该是:
char *p = "a";
或者,如果你想将指针指向一个字符变量,你可以使用以下方式:
char a = 'a';
char *p = &a;
这样,指针 p 将指向字符变量 a。
【attention】
static函数与普通函数作用域不同,仅在本文件。只在当前源文件中使用的函数应该说明为内部函数(static)。存放在代码区。
在c++中,静态成员函数可以用类名直接调用,也就是你不需要实例化一个对象就可以调用这个函数。但是这样一来,你也就不能传递"this指针"进去,所以这种函数是不能操作非static成员或者调用非static成员函数的。也可以说所有实例对象共享静态成员函数。
变量与关键字
extern关键字:
在多文件编译中体现:
extern:外部变量
extern int a;//告诉编译器,我这个a是外面的不是我这个.c文件里面。
extern 只能修饰全局变量:你要使用外部变量的意思
外部变量定义的时候不需要加extern 默认全局变量就是外部变量
声明的时候加就extern可以了
函数默认的就是全局变量,而且默认声明就是外部变量,也就是在声明函数的时候可以不用特意写extern
static关键字:
static关键字的作用:
改变生命期 和 限制作用域 。(设置变量的作用域,决定变量的存储域)
如:
修饰inline函数:限制作用域
修饰普通函数:限制作用域
修饰局部变量:改变生命期
修饰全局变量:限制作用域
static:静态变量() 设置变量的作用域(给函数定义的),决定变量的存储域(给基本变量定义成静态)
static int a: 只能在自己的作用域内使用 (拿去修饰全局变量的时候,这个全局变量就不能给其他.c使用了)
static int a: 修饰局部变量 只初始化一次,而且函数退出后静态变量的空间还在,没有跟着函数被释放
修饰函数:因为函数默认是外部变量的可以给其他.c调用,如果被你改成static修饰,则函数就只能在自己的.c里面被调用
静态变量的声明是否会被执行多次 ?
如下部分,当这个函数被执行多次,声明部分,是否会被多次执行呢?
#include <stdio.h>
void fun()
{
static int a = 1;
printf("%d\t",a);
return;
}
int main()
{
fun();
fun();
fun();
return 0;
}
执行结果为:
1 2 3
void fun() 中的 static int a 在第2次执行时,没有被置 1 。
推论:静态声明的变量,声明时的赋值仅在,声明的首次被执行。
(1) 静态局部变量在静态存储区内(data段(初始化)/.bss段(未初始化))分配存储单元。在程序整个运行期间都不释放。而自动变量(即动态局部变量)属于动态存储类别,存储在动态存储区空间(而不是静态存储区空间),函数调用结束后即释放。
(2) 为静态局部变量赋初值是在编译时进行的,即只赋初值一次,在程序运行时它已有初值。以后每次调用函数时不再重新赋初值而只是保留上次函数调用结束时的值。但是为自动变量赋初值,不是在编译时进行的,而是在函数调用时进行,每调用一次函数重新给一次初值,相当于执行一次赋值语句。
(3) 如果在定义局部变量时不赋初值的话,对静态局部变量来说,编译时自动赋初值0(对数值型变量)或空字符(对字符型变量)。而对自动变量来说,如果不赋初值,则它的值是一个不确定的值。这是由于每次函数调用结束后存储单元已释放,下次调用时又重新另分配存储单元,而所分配的单元中的值是不确定的。
(4) 虽然静态局部变量在函数调用结束后仍然存在,但其他函数是不能引用它的,也就是说,在其他函数中它是“不可见”的
const 关键字:
const 关键字倒是比较明了,用const修饰的全局变量放在 rodata (only read)里,字符串默认就是常量。
对const,注意以下几点就行了。
指针常量:指向的数据是常量。如 const char* p = “abc”; p指向的内容是常量 ,但p本身不是常量,你可以让p再指向”123”。
常量指针:指针本身是常量。如:char* const p = “abc”; p本身就是常量,你不能让p再指向”123”。 指针常量 + 常量指针:指针和指针指向的数据都是常量。const char* const p =”abc”; 两者都是常量,不能再修改。
violatile关键字:
通常用来修饰多线程共享的全局变量和IO内存。告诉编译器,不要把此类变量优化到寄存器中,每次都要老老实实的从内存中读取,因为它们随时都可能变化。
这个关键字可能比较生僻,但千万不要忘了它,否则一个错误让你调试好几天也得不到一点线索
auto关键字:
自动化变量(所有局部变量默认是自动化变量)一般局部变量的空间存放在栈空间里,函数退出他的空间也会跟着释放,属于动态存储类别,占动态存储空间,函数调用结束后即释放,那你如果还去获取它的数据,那就是非法内存地址访问。
register关键字:(寄存器变量)
寄存器变量,不是存储在内存里面的,所有这种变量没有内存地址,所以不能&寄存器变量名,
为了提高访问效率.(获取数据的时候不需要在从内存中获取了,直接从寄存器里面获取)
register int a;
printf("%p\n",&a); //编译会报错 &只能去内存中的地址
寄存器变量
在程序运行时,根据需要到内存中相应的存储单元中调用,如果一个变量在程序中频繁使用,例如循环变量,那么,系统就必须多次访问内存中的该单元,影响程序的执行效率。因此,C语言\C++语言还定义了一种变量,不是保存在内存上,而是直接存储在CPU中的寄存器中,这种变量称为寄存器变量。
register int i=100;
- C编译程序会自动地将寄存器变量变为自动变量
- 由于受硬件寄存器长度的限制,所以寄存器变量只能是char、int或指针型。寄存器说明符只能用于说明函数中的变量和函数中的形参,因此不允许将外部变量或静态变量说明为"register"
- register变量使用的是硬件CPU中的寄存器,寄存器变量无地址,所以不能使用取地址运算符"&"求寄存器变量的地址。
寄存器变量速度测试
对于VC编译器会自动优化,即使没有声明寄存器变量,VC也会自动优化。
对于GCC编译器就不会自动优化。
include <stdio.h>
include<stdlib.h>
include<time.h>
int main()
{
register int temp, i;
//int temp, i;
time_t start, end;
time(&start);//获取当前时间
for (i = 0; i <= 30000000; i++)
for (temp = 0; temp <= 100; temp++);
printf("ok\n");
time(&end);
printf("%d\n",(unsigned int)(end-start));
return 0;
}
gcc编译器来编译,用寄存器变量执行时间为2秒
不用寄存器变量执行时间为6秒。
volatile变量
程序中常用的变量编译器为了提高CPU的运行效率,会把变量的值备份在寄存器中,方便读取和访问,就不需要每次从该变量的地址下重新读取该变量的值,如果此时内存中的变量值已经发生变化,则CPU是不知道的,还是使用备份在寄存器中的变量的值。
volatile具有易变的意思,如果用该关键字修饰一个变量,可以防止编译器对变量进行优化,也就是每次CPU每次使用该变量的时候,都重新从变量的地址中读取变量的值,所以每次读取的值都是最新的值。
volatile
是 C 和 C++ 中的一个关键字,用于告诉编译器不要对其所修饰的变量进行优化,以确保对该变量的访问符合程序员的意图。volatile
的主要作用是告诉编译器,所修饰的变量的值可能会在程序执行过程中被意外修改,因此编译器不应该对这些变量的访问进行优化或者缓存,而是应该每次都直接从内存中读取或写入这些变量的值。
以下是关于 volatile
关键字的一些重要点:
- 防止优化: 编译器在编译过程中会对变量进行各种优化,包括常量折叠、寄存器分配等。使用
volatile
关键字可以告诉编译器,不要对这些变量进行优化,因为它们的值可能会在程序运行时被外部因素改变。 - 多线程同步: 在多线程程序中,
volatile
关键字可以用于确保变量在多个线程之间的可见性。当一个线程修改了volatile
变量的值时,其他线程能够立即看到这个变化,而不会使用之前缓存的值。 - I/O 端口和中断处理: 在嵌入式系统中,
volatile
常用于访问 I/O 端口和处理中断。因为这些操作通常与外部硬件相关,并且可能会在任何时候发生变化,因此需要确保对这些变量的访问是实时的,而不会被编译器优化。 - 信号处理: 在信号处理程序中,通常会使用
volatile
来修饰被信号处理器修改的全局变量,以确保对变量的访问是及时的,并且不会受到编译器的优化影响。
尽管 volatile
可以确保变量的实时性和可见性,但它并不能保证原子性。在多线程环境中,如果要确保对变量的操作是原子的,通常需要使用互斥锁或者原子操作等机制来保护 volatile
变量。
需要注意的是,虽然 volatile
可以告诉编译器不要对变量进行优化,但它并不能保证在多线程环境中的线程安全性。在多线程编程中,还需要使用其他同步机制来确保线程安全性。
三个应用案例:
1. 在多线程中使用的全局变量,应该使用volatile进行修饰
2. 在使用处理器寄存器的时候,应该使用volatile进行修饰
3. 终端服务函数中的全局变量,应该使用volatile进行修饰
浮点数
小数的输出 %e的用法
%e是printf的格式控制字符,用于指定浮点类型数据使用指数形式输出
浮点类型分为整数部分和小数部分,它们由点号.分隔,
例如 0.0、75.0、4.023、0.27、-937.198 -0.27 等都是合法的小数,这是最常见的小数形式,我们将它称为十进制形式。
此外,小数也可以采用指数形式,例如 7.25×10^2、 0.0368×10^5、 100.22×10^-2、 -27.36×10^-3 等。任何小数都可以用指数形式来表示。
C语言同时支持以上两种形式的小数。但是在书写时,C语言中的指数形式和数学中的指数形式有所差异。
C语言中小数的指数形式为:aEn 或 aen
a 为尾数部分,是一个十进制数;n 为指数部分,是一个十进制整数;E或e是固定的字符,用于分割尾数部分和指数部分。整个表达式等价于 a×10n。
指数形式的小数举例:
2.1E5 = 2.1×105,其中 2.1 是尾数,5 是指数。
3.7E-2 = 3.7×10-2,其中 3.7 是尾数,-2 是指数。
0.5E7 = 0.5×107,其中 0.5 是尾数,7 是指数。
%.2e 是尾数小数点后面保留两位
合法的浮点数:
- 十进制小数形式。由数字和小数点组成,必须有小数点。例如(123.)(123.0)(.123)。
- 指数形式。如123e3。字母e(或E)之前必须有数字,e后面的指数必须为整数。
- 规范化的指数形式里面,小数点前面有且只有一位非零的数字。如1.2345e8
e后面要有整数
50.是合法的
书写浮点常量有多种选择,,基本形式:包含小数点的一个带符号的数字序列,接着是字母e或E,然后是一个代表10的指数的有符号值,且必须为整数。
其中 e 或 E 被称为阶码标志,e 或 E 后面的有符号整数被称为阶码。阶码代表 10 的阶码次方。
例如:
-1.56E+12;
2.87e-3;
-8e1.0;错
浮点型常量可以省略正号,可以没有小数点或指数部分。
例如:
2E5;
19.28;
但是不能同时没有二者。
例如:
19;错
浮点型常量可以省略小数或者整数部分,但不能同时省略。
例如:
e3;错
注意:e前后的实数和整数不能省略。
-80.0e;错
注意:浮点型常量中不能有空格!例如:
3.21e -12 /* 有空格,错! */
3.14e5 /* 有空格,错! */
注意:浮点型常量有阶码的小数点前面只能有一位非0的数字。
例如:
1.81e7;//对
18.1e6;//错
另外阶码后面只能是整数,不能是整数表达式。
5.0e(1+4);//错
字符数组
字符数组初始化
在C语言中,字符串是当做字符数组来处理的;所以字符串有两种声明方式,一种是字符数组,一种是字符指针。
(1)直接逐个初始化字符数组:字符数组的初始化,最容易理解的方式就是逐个字符赋给数组中各元素。
char str[10]={ 'I',' ','a','m',' ','h','a','p','p','y'}*;*
注意:如果花括号中提供的字符个数大于数组长度,则按语法错误处理;若小于数组长度,则只将这些字符数组中前面那些元素,其余的元素自动定为空字符(即'\0' 。
(2)用字符串常量来初始化字符数组:在c语言中,将字符串作为字符数组来处理。因此可以使用字符串来初始化字符数组。`
char str[]={"I am happy"}*;*
也可以省略花括号:
char str[]="I am happy"*;*
但是,上述这种字符数组的整体赋值只能在字符数组初始化时使用,不能用于字符数组的赋值,字符数组的赋值只能对其元素一一赋值,下面的赋值方法是错误的。
char str[]*;*
*str*="I am happy"*;
//错误,字符数组的赋值只能按元素一一赋值(错误的原因: C语言并没有提供可以直接操作字符串的运算符;“=”可以用于其他数据类型的赋值,但是不可以直接给字符串赋值。*
这是字符数组初始化的两种方式,但是这两种方式其实是不等价的;他们的数组长度不同。
#include<stdio.h>
int main()
{
char parr[] = "zifuchuanshuzu"*;*
char charr[] = { 'z','i','f','u','c','h','u','a','n','s','h','u','z','u' }*;*
char charr_test[] = { 'z','i','f','u','c','h','u','a','n','s','h','u','z','u' ,'\0'}*;*
int *num_parr* = sizeof(parr)*;*
int *num_charr* = sizeof(charr)*;*
int *num_charr_test* = sizeof(charr_test)*;*
printf("The parr[] is : %s\n", parr)*; //zifuchuanshuzhu*
printf("The size of parr[] is : %d\n\n", num_parr)*; //15*
//与charr[]不等价
printf("The charr[] is : %s\n", charr)*; //zifuchuanshuzu乱码*
printf("The size of charr[] is : %d\n\n", num_charr)*; //14*
//等价于charr[]
printf("The charr_test[] is : %s\n", charr_test)*; //zifuchuanshuzhu*
printf("The size of charr_test[] is : %d\n", num_charr_test)*; //15*
return 0*;*
}
从结果可以看到第二种初始化方式,打印的结果有问题,但是字符数量没有问题。这是因为字符串默认是以’\0’结束的,第二种初始化方式中没有’\0’,而我们以字符串方式打印,所以出错;
第一种是系统自动添加了’\0’;我们可以看到其字符数量是15(与第三种相同)。
(3)字符指针:在C语言中我们也可以使用字符指针来存储字符串。
字符指针初始化:
char* *str*="zifuchuanshuzu"*;*
C语言对字符串常量是按照字符数组来处理的,在内存中开辟了一个字符数组用来存放字符串常量,程序在定义字符串指针变量str时,只是把字符串首地址赋值给str。
printf("%s\n",str);
系统首先输出str指向的字符,而后自加1,直至遇到’\0’;与数组的输出方式相同。
字符指针的赋值:
char *str*;*
*str*="zifuzhizhen"*;*
对于字符指针这种赋值方式是正确的。与字符数组不同。
这种初始化方式是不行的
char *str*;*
scanf("%s", str)*;*
字符串操作库函数
strrchr()函数
char *strrchr(const char *str, int c)
参数
str -- C 字符串。
c -- 要搜索的字符,通常以整数形式传递(ASCII 值),但是最终会转换回 char 形式。
返回值
strrchr() 函数从字符串的末尾开始向前搜索,直到找到指定的字符或搜索完整个字符串。
如果找到字符,它将返回一个指向该字符的指针,否则返回 NULL。
演示:
#include <stdio.h>
#include <string.h>
int main ()
{
int len;
const char str[] = "https://www.runoob.com";
const char ch = '.';
char *ret;
ret = strrchr(str, ch);
printf("|%c| 之后的字符串是 - |%s|\n", ch, ret);
return(0);
}
结果:|.| 之后的字符串是 - |.com|
以下实例 strrchr() 函数在字符串 "Hello, World!" 中查找字符 'o',并返回最后一个 'o' 的位置:
#include <stdio.h>
#include <string.h>
int main() {
const char *str = "Hello, World!";
char ch = 'o';
char *lastO = strrchr(str, ch);
if (lastO != NULL) {
printf("Last '%c' found at position: %ld\n", ch, lastO - str);
}
else {
printf("'%c' not found in the string.\n", ch);
}
return 0;
}
//结果:"Last 'o' found at position: 8"
strchr函数:
char *strchr(const char *str, int c)
参数
str -- 要查找的字符串。
c -- 要查找的字符。
返回值
如果在字符串 str 中找到字符 c,则函数返回指向该字符的指针,如果未找到该字符则返回 NULL。
#include <stdio.h>
#include <string.h>
int main ()
{
const char str[] = "https://www.runoob.com";
const char ch = 'o';
char *ptr;
ptr = strchr(str, ch);
if (ptr != NULL) {
printf("字符 'o' 出现的位置为 %ld。\n", ptr - str + 1);
printf("|%c| 之后的字符串是 - |%s|\n", ch, ptr);
}
else {
printf("没有找到字符 'o' 。\n");
}
return(0);
}
//结果:字符 'o' 出现的位置为 16。
|o| 之后的字符串是 - |oob.com|
strcat函数
char *strcat(char *dest, const char *src)
参数
dest -- 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串。
src -- 指向要追加的字符串,该字符串不会覆盖目标字符串。
返回值
该函数返回一个指向最终的目标字符串 dest 的指针。
#include <stdio.h>
#include <string.h>
int main ()
{
char src[50], dest[50];
strcpy(src, "This is source");
strcpy(dest, "This is destination");
strcat(dest, src);
printf("最终的目标字符串: |%s|", dest);
return(0);
}
最终的目标字符串: |This is destinationThis is source|
strcpy函数
char *strcpy(char *dest, const char *src)
参数
dest -- 指向用于存储复制内容的目标数组。
src -- 要复制的字符串。
#include <stdio.h>
#include <string.h>
int main()
{
char src[40];
char dest[100];
memset(dest, '\0', sizeof(dest));
strcpy(src, "This is runoob.com");
strcpy(dest, src);
printf("最终的目标字符串: %s\n", dest);
return(0);
}
最终的目标字符串: This is runoob.com
#include <stdio.h>
#include <string.h>
int main ()
{
char str1[]="Sample string";
char str2[40];
char str3[40];
strcpy (str2,str1);
strcpy (str3,"copy successful");
printf ("str1: %s\nstr2: %s\nstr3: %s\n",str1,str2,str3);
return 0;
}
str1: Sample string
str2: Sample string
str3: copy successful
strcpy()遇到\0会停止拷贝并且会把\0拷贝进去
strcpy(str1,str2)会将str2后面的\0也拷贝进去 str1原来后面的数据还在但是不读了
strcmp函数
strcmp()在新版的编译器是返回ASCII码的值的差
老版本的编译器返回的是1或者-1
strstr函数
strstr
是一个C标准库函数,用于在一个字符串中查找第一次出现另一个字符串的位置。它的声明如下:
char *strstr(const char *haystack, const char *needle);
其中,haystack
是要搜索的目标字符串,needle
是要查找的子字符串。strstr
函数返回一个指向第一次出现 needle
子字符串的位置的指针,如果未找到,则返回空指针(NULL
)。
示例用法:
#include <stdio.h>
#include <string.h>
int main() {
const char *haystack = "Hello, World!";
const char *needle = "World";
char *result = strstr(haystack, needle);
if (result != NULL) {
printf("'%s' found at position: %ld\n", needle, result - haystack);
} else {
printf("'%s' not found in '%s'\n", needle, haystack);
}
return 0;
}
在这个示例中,haystack
是目标字符串,needle
是要查找的子字符串。如果子字符串存在于目标字符串中,则打印出子字符串的位置;否则打印出未找到的消息。
需要注意的是,strstr
函数是区分大小写的,如果需要不区分大小写的查找,可以使用其他函数,如 strcasestr
(GNU扩展)或自定义函数来实现。
strtok函数
strtok
函数是C标准库中的一个字符串处理函数,用于将字符串拆分成一系列标记(token)。它可以根据指定的分隔符将一个字符串分割成多个子字符串,并逐个返回这些子字符串。
strtok
函数的原型如下:
char *strtok(char *str, const char *delim);
其中,str
是要分割的字符串,delim
是分隔符字符串。strtok
函数在第一次被调用时,需要传递要分割的字符串 str
,并将 delim
中的任一字符作为分隔符来分割 str
。在后续调用时,可以将 str
设置为 NULL
,继续使用上一次调用的分割状态。
strtok
函数返回指向分割后的子字符串的指针。如果没有找到更多的子字符串,则返回 NULL
。
需要注意的是,strtok
函数会修改原始字符串,它会在每个分隔符处插入空字符(\0
),将原始字符串切割成多个子字符串。因此,每次调用 strtok
函数时,它会返回一个新的子字符串,直到没有更多的子字符串可返回为止。
以下是一个示例用法:
例1:
#include <stdio.h>
#include <string.h>
int main() {
char str[] = "apple,banana,orange";
const char delim[] = ",";
char *token = strtok(str, delim);
while (token != NULL) {
printf("%s\n", token);
token = strtok(NULL, delim);
}
return 0;
}
例2:
buf[1024] = "name:zhang|age:18|id:1234"
p1 = strtok(buf,"|"); p1->name:zhang
p2 = strtok(NULL,""); p2->age:18|id:1234 !!会将后续的全部输出
p3 = strtok(NULL,"|"); p3->age:18
在这个示例中,str
是要分割的字符串,delim
是分隔符。首先调用 strtok
函数来获取第一个子字符串,然后在循环中继续调用 strtok(NULL, delim)
来获取下一个子字符串,直到所有子字符串都被提取完毕。
gets()
描述
C 库函数 char *gets(char *str) 从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
char *gets(char *str)
参数
str -- 这是指向一个字符数组的指针,该数组存储了 C 字符串。
返回值
如果成功,该函数返回 str。如果发生错误或者到达文件末尾时还未读取任何字符,则返回 NULL。
#include <stdio.h>
int main()
{
char str[50];
printf("请输入一个字符串:");
gets(str);
printf("您输入的字符串是:%s", str);
return(0);
}
请输入一个字符串:runoob
您输入的字符串是:runoob
puts函数
puts(c_array); //这个函数只能打印输出字符类型的数据,可以输出字符串
int puts(const char *str)
参数
str -- 这是要被写入的 C 字符串。
返回值
如果成功,该函数返回一个非负值为字符串长度(包括末尾的 \0),如果发生错误则返回 EOF。
#include <stdio.h>
#include <string.h>
int main()
{
char str1[15];
char str2[15];
strcpy(str1, "RUNOOB1");
strcpy(str2, "RUNOOB2");
puts(str1);
puts(str2);
return(0);
}
//输出:RUNOOB1 RUNOOB2
strncpy函数
char *strncpy(char *dest, const char *src, size_t n)
参数
dest -- 指向用于存储复制内容的目标数组。
src -- 要复制的字符串。
n -- 要从源中复制的字符数。
返回值
该函数返回最终复制的字符串。
#include <stdio.h>
#include <string.h>
int main()
{
char src[40];
char dest[12];
memset(dest, '\0', sizeof(dest));
strcpy(src, "This is runoob.com");
strncpy(dest, src, 10);
printf("最终的目标字符串: %s\n", dest);
return(0);
}
输出:最终的目标字符串: This is ru
memset函数
void *memset(void *str, int c, size_t n)
参数
str -- 指向要填充的内存块。
c -- 要被设置的值。该值以 int 形式传递,但是函数在填充内存块时是使用该值的无符号字符形式。
n -- 要被设置为该值的字符数。
返回值
该值返回一个指向存储区 str 的指针。
#include <stdio.h>
#include <string.h>
int main ()
{
char str[50];
strcpy(str,"This is string.h library function");
puts(str);
memset(str,'$',7);
puts(str);
return(0);
}
输出:This is string.h library function
$$$$$$$ string.h library function
strcmp()函数
int strcmp(const char *str1, const char *str2)
参数
str1 -- 要进行比较的第一个字符串。
str2 -- 要进行比较的第二个字符串。
返回值
该函数返回值如下:
如果返回值小于 0,则表示 str1 小于 str2。
如果返回值大于 0,则表示 str1 大于 str2。
如果返回值等于 0,则表示 str1 等于 str2。
#include <stdio.h>
#include <string.h>
int main ()
{
char str1[15];
char str2[15];
int ret;
strcpy(str1, "abcdef");
strcpy(str2, "ABCDEF");
ret = strcmp(str1, str2);
if(ret < 0)
{
printf("str1 小于 str2");
}
else if(ret > 0)
{
printf("str1 大于 str2");
}
else
{
printf("str1 等于 str2");
}
return(0);
}
//输出str1 大于 str2
memset函数的用法详解
最近做题时突然想到用memset给数组的元素赋值100,但是发现结果和想象中的不一样
通过debug看到数组中的元素的值都为1684300900,而明明给它的是100,这是为什么呢,于是查阅了一下memset函数的用法和原理
memset()函数介绍
首先来看函数原型
void *memset(void *str, int c, size_t n)
解释:复制字符 c(一个无符号字符)到参数 str 所指向的字符串的前 n 个字符。
作用:是在一段内存块中填充某个给定的值,它是对较大的结构体或数组进行清零操作的一种最快方法
头文件:C中#include<string.h>,C++中#include
看着介绍其实函数作用非常简单,就是用于初始化,但是需要注意的是memset赋值的时候是按字节赋值,是将参数化成二进制之后填入一个字节。就比如前面的例子中,想要通过memset(a,100,sizeof a)给int类型的数组赋值,你给第一个字节的是一百,转成二进制就是0110 0100,而int有四个字节,也就是说,一个int被赋值为
0110 0100,0110 0100,0110 0100,0110 0100,对应的十进制是1684300900,根本不是你想要赋的值100,这也就解释了为什么数组中的元素的值都为1684300900。
memset赋值时只能赋值为0?
答案肯定不是,比如任意字符都是可以的,初始化成0是最常用的。int类型的一般都是赋值0或-1,其他的值都不行。
结论
为地址str开始的n个字节赋值c,注意:是逐个字节赋值,str开始的n个字节中的每个字节都赋值为c。
(1) 若str指向char型地址,value可为任意字符值;
(2) 若str指向非char型,如int型地址,要想赋值正确,value的值只能是-1或0,因为-1和0转化成二进制后每一位都是一样的,设int型占4个字节,则-1=0XFFFFFFFF, 0=0X00000000。
举例:给数组赋值-1
int A[2];
memset(A, -1, sizeof A);
memset初始化为无穷大
memset(a , 0x3f , sizeof a);
通过memset函数的介绍,上述初始化是将数组a的每个元素赋值为0x3f3f3f3f。0x3f3f3f3f 真的是个非常精巧的常量
他的十进制是 1061109567也就是109级别的,和0x7fffffff一个数量级,0x7fffffff是int类型的最大值,即231-1=2,147,483,647。
memcpy()
描述
C 库函数 void *memcpy(void *dest, const void *src, size_t n); 从存储区 src 复制 n 个字节到存储区 dest。
声明
下面是 memcpy() 函数的声明。
void *memcpy(void str1, const void str2, size_t n)
参数
str1 -- 指向用于存储复制内容的目标数组,类型强制转换为 void 指针。
str2 -- 指向要复制的数据源,类型强制转换为 void 指针。
n -- 要被复制的字节数。
返回值
该函数返回一个指向目标存储区 str1 的指针。
实例
下面的实例演示了 memcpy() 函数的用法。
// 将字符串复制到数组 dest 中
include <stdio.h>
include <string.h>
int main ()
{
const char src[50] = "http://www.runoob.com";
char dest[50];
memcpy(dest, src, strlen(src)+1);
printf("dest = %s\n", dest);
return(0);
} //dest = http://www.runoob.com
将 s 中第 11 个字符开始的 6个连续字符复制到 d 中:
include <stdio.h>
include<string.h>
int main()
{
char s="http://www.runoob.com";
char d[20];
memcpy(d, s+11, 6);// 从第 11 个字符(r)开始复制,连续复制 6 个字符(runoob)
// 或者 memcpy(d, s+11sizeof(char), 6*sizeof(char));
d[6]='\0';
printf("%s", d);
return 0;
} //runoob
include<stdio.h>
include<string.h>
int main(void)
{
char src[] = "***";
char dest[] = "abcdefg";
printf("使用 memcpy 前: %s\n", dest);
memcpy(dest, src, strlen(src));
printf("使用 memcpy 后: %s\n", dest);
return 0;
} //使用 memcpy 前: abcdefg
使用 memcpy 后: ***defg
方法4:strcpy:字符串拷贝函数,strnpyc,memcpy(内存拷贝:只要是数据就能拷贝,不考虑数据类型)
数组赋值:
for(int lp=0; lp<len; lp++)
{
c_array[lp] = getchar(); //一个一个输入 scanf("%c",)
}
gets(c_array);//一次性输入
scanf("%s",c_arrya);//一次性输入 scanf里面也是循环一个一个的去读取键盘的数据,一个一个赋值
`void test7()`
`{`
`char *p = "assdfghgfdsdfghgfdsasdfgfddfghnm";
`memcpy(p,"暴击",strlen("暴击"));
`printf("%s\n",p);
`}`
`int main()`
`{`
`test7();
`return 0;
`} //段错误`
`因为p就八个字节 字符串常量返回的是一个首地址八个字节`
结构体
写法一:定义全局结构体类型顺便定义全局结构体变量
struct xxx
{
int a;
int b;
char c;
}YYY,XXX,OOO,UUU,TTT*;
//定义类型的时候顺便定义变量*
//全局结构体类型,全局结构体变量
int main()
{
YYY.a =110;
YYY.b = 99;
YYY.c = 'S';
return 0;
}
写法二:
struct xxx
{
int a;
int b;
char c;
}YYY = {1,111,'s'},XXX = {2,222,'o'},OOO = {3,333,'s'},UUU,TTT;//定义类型的时候顺便定义变量并且初始化
int main()
{
printf("%d--%d--%c\n",YYY.a,YYY.b,YYY.c);
return 0;
}
写法三:
struct xxx
{
int a;
int b;
char c;
};
int main()
{
struct xxx UUU = {1,'S'};//在定义结构体变量的时候初始化它 局部变量
printf("%d---%d---%c\n",UUU.a,UUU.b,UUU.c);
return 0;
}
写法四:
struct
{
int a;
int b;
char c;
}xxx,ooo,yyy;//没有写类型名字的,只能再最后的分号前面定义变量,不能中途定义变量了
结构体变量是不是和数组变量一样,不能直接赋值?
答:能,但是结构体的类型要一样
结构体的大小不是简单地数据类型大小相加,需要内存对齐
struct Person
{
char a; //所占地址 0 ~ 3 后三个字节为空不占
int b; //4 ~ 7
char c; //8 ~ 11
int d; //12 ~ 15
} //一共占16个字节
内存对齐:
好处:以空间换时间
对齐模数:
#pragma pack (show) //放在文件开头查看对齐模数,在编译结束控制台显示
#pragma pack (1) //更改对齐模数为1,对齐模数只能更改为2的整数倍
内存对齐原则:
第一属性开始,从0开始计算偏移量
第二属性要放在该 min(属性的大小,对齐模数)的整数倍上
将上面计算结果扩充到这个结构体中最大数据类型与对齐模数取最小值的整数倍上
vs上默认对齐模数为8
由于存储变量地址对齐的问题,结构体大小必须满足三条原则
一,结构体变量的首地址(也是结构体首成员的首地址)必须是结构体"最宽基本类型成员"大小的整数倍
二,结构体每个成员相当于结构体首地址的偏移量,都是该成员大小的整数倍 偏移量 = 每一个成员的地址 - 首成员的地址
三,结构体的大小为结构体"最宽基本类型成员"大小的整数倍
计算偏移量:
struct s
{
char a;
int b;
};
//计算b的偏移量
struct s s1;
struct s* sp = &s1;
printf("b的偏移量是%d",(int)(&(sp->b))-(int)sp); //可以将地址转化为int型
或者宏函数offsetof(struct,elem);在头文件<stddef.h>
printf("b的偏移量是%d",offsetof(s1,b));
struct S
{
char a;
int b;
struct s;
};
struct S s2;
//怎么找到s中b的偏移量
offsetof1 = offsetof(struct S,s); //找到S中s的偏移量
offsetof2 = offsetof(struct s,b); //找到s中b的偏移量
//访问s中的b
printf("s中b的值为%d",(int)((char)&s2+offsetof1+offsetof2)); //!!!!!
printf("s中b的值为%d",((struct s)((char)&s2+offsetof1))->b); //!!!!!
struct Student
{
int a; 0~3
char b; 4~7
double c; 8~15
float d; 16~19
};
printf("结构体的内存大小为:%d",sizeof(struct Student)); //24 min(对齐模数8 最大数据类型double8)的整数倍
结构体内存大小为最大数据元素的整数倍
#pragma pack (1) //设置对齐模数为1
struct Student
{
int a; //0~3
int c; //4~7
char b; //8
};
sizeof(struct Student); 输出:9 //0~8共9个字节 min(对齐模数1,最大数据类型int 4)的整数倍
嵌套结构体内存大小
struct b
{
char a; //0 ~ 7
struct student b; //8~31 //struct student的大小为24里面最大类型为8字节所以从地址8开始
double c; //32~39
};
struct b的内存大小为40 不能将struct student看做最大的结构体类型,把他拆开看,他里面元素的最大结构体类型为8
当结构体中嵌套结构体时,子结构体要放在子结构体中最大数据类型的整数倍上
指针
指针介绍
指针是地址,地址是十六进制,所以指针就是常量,指针变量可以用来存放指针,譬如:整型变量可以存放整型
指针就是__变量__在__运行内存的__地址,地址就是指针。数组名是首元素的地址,首元素的地址可以表示数组名字
基本概念 --- int a=10; &a.有人会误认a存放了&a, 而是a加上取地址符可以表示a的首地址而已。&a是_常__量,不能++(&a)&a:这个东西就是一个指针,这种表达方式并没有体现出这个指针是存放在某个变量里面的,那是常量了。譬如 :1++(不行)
int a=0; //a是表示一块空间,这块空间有4个字节,&a表示这块空间的首地址
a:a里面存放了0,a是一个变量,所以a有地址
那也就是说&a不能改变,即 printf("%d\n",(&a)++); &a+1 (行)
但是用个指针变量指向他这个指针变量是可以改变的
野指针
int b = 10;
intq; //野指针*
*q = b;
//这种写法是不允许的,因为q是野指针存放的是随机地址,*q通过这个随机地址获得一块随机的空间去存放10
//这块空间当然是8个字节的,但是这块空间是随机的也就是很大的几率是不能正常访问的,是不合法的
最后,总结一下野指针的的成因吧
1、指针变量没有被初始化。任何指针变量刚被创建时不会自动成为NULL指针,它的默认值是随机的,它会乱指一气
2、指针p被free或者delete之后,没有置为NULL,让人误以为p是个合法的指针
3、指针操作超越了变量的作用范围。这种情况让人防不胜防
空指针
将指针置空是无法访问的 int* p = NULL; //虽然p不是野指针了但是它现在存放的是NULL地址(0),这个地址是低地址不属于用户空间不能访问
int* a_p;
int a = 99;
a_p = &a;
printf("a = %d\n", a_p); 这样还可以,但是非法*
int* a_p;
int a = 99;
*a_p = a;
printf("a = %d\n", a_p); //这样是不可以的,因为修改了a_p指向非法地址的值*
int a=20,p,**q = &a; *p=*q; //非法
字符指针和字符串指针
char *p = 'a';
在C语言中,你声明了一个字符指针 p,但是赋值的方式是不正确的。单引号 'a' 表示一个字符常量,而不是一个字符串。如果你想将一个字符赋给指针变量,你可以使用双引号 "a" 来表示一个包含该字符的字符串。正确的代码应该是:
char *p = "a";
或者,如果你想将指针指向一个字符变量,你可以使用以下方式:
char a = 'a';
char *p = &a;
这样,指针 p 将指向字符变量 a。
char *p =
"asdfghj"; //常量指针
++*p是不可以的 *p指向的是常量 // *和++同一优先级 从右向左算
但是char c = 'a';
char *p1 = &c;
++*p;是可以的因为他是字符变量
在C语言中,指针可以指向字符常量。你可以声明一个const char*
类型的指针变量,并将其指向一个字符常量。
以下是一个示例:
const char* p = 'a';
在这个示例中,我们声明了一个指向字符常量的指针 p
。通过使用 const
关键字,我们明确指示指针所指向的内容是不可修改的。
你可以使用这个指针来访问字符常量的值。例如,可以通过解引用指针来获取字符常量的值:
char c = *p;
需要注意的是,因为字符常量是不可修改的,所以不能通过指针来修改字符常量的值。如果你尝试去修改字符常量,会导致未定义的行为。所以在声明指向字符常量的指针时,应该使用 const
修饰符来确保不会通过指针修改字符常量的值。
void指针
无类型能定义变量?
无类型 是不能定义变量申请空间的。
void * 变量,不能直接解引用,能直接赋值。
注意:void是无类型,void*是万能指针类型,万能指针类型void *能定义变量也能赋值但是不能解引用,而void不能定义
int main()
{
int a = 300; //1
void * p= &a; //void *表示指针类型,
}
但是不知道是哪一种指针类型,所以称为万能(可以转换成任意指针类型)指针类型
printf("%d\n",((char )p)); // int * p; *p 获取4个字节的空间 char *p; *p获取1个字节的空间
//因为是万能指针,所以万能指针解引用会导致编译器不知道去访问多少字节的空间
return 0;
}
void * :一级万能指针(无类型指针)只是书面上(字面上)的意思,其实任意指针类型都可以转成任意指针类型,因为同一等级的指针的大小在同一个系统位数上都是一样的,
因为大小确保了空间是___足够__的。很少不同等级的指针变量互相转换。
if(data_type == DATA_TYPE_INT)
printf("%d\n",((int )p)); //解引用:你提供一个地址去解引用,编译器就会通过你给的地址和地址的类型去获取对应空间
if(data_type == DATA_TYPE_CHAR)
printf("%c\n",((char )p));
if(data_type == DATA_TYPE_STR)
printf("%s\n",(char )p);*
return 0;
}
int main()
{
int a = 10;
char b = 'S';
Fun((void )&a,DATA_TYPE_INT);// p = &a;*
Fun((void )&b,DATA_TYPE_CHAR);*
Fun((void )"你好,我是旦丁!",DATA_TYPE_STR);*
//void * p = "你好,我是旦丁!"
return 0;
}
注意:类型转换要考虑空间一不一样,找一个和指针变量大小一样的类型去类型转换.
malloc(unsigned int data_type)
int *p = malloc(12);
p+1 //是加4个字节
*(p+2) = 3 //对第三个整型空间赋值
calloc --- 按字节块来申请空间的,你要确定你要多少块空间,每块空间要多少字节?
它calloc虽然是按照块来申请空间的,但是每一块空间的地址都是连续的,那多块和一块是没有区别的.只是在敲代码的时候比较形象一点
calloc: 5块 每块4个字节 20个字节 calloc(5,4) //
② calloc申请的空间是会帮你清0的,但是分块没什么作用
③ free函数的形参只能是malloc 或者 calloc的返回值
笔试题:
//假设在一个函数里面定义一个局部变量,然后我想在函数外面使用这个变量,怎么解决:
假设在一个函数里面定义一个局部变量,然后我想在函数外面使用这个变量指向的空间,怎么解决:
答案一:偏向于C++
返回这个变量设置成static,然后返回其地址即可。 (C)
int * Fun();
int * Fun()
{
static int data;
return &data;
}
int main()
{
int * data_p = Fun();
*data_p = 110;
return 0;
}
譬如:
char * Fun()//指针函数:他是一个函数,返回值是指针
{
//static int a = 10;
// return a;
char p = (char )malloc(100); //虽然p存放的是malloc给的堆空间首地址,但是p本身是局部变量他的8个字节空间是栈空间。
return p;
}
int main() //上一级函数
{
char * new_p = Fun();//把Fun里面的局部指针变量p指向的空间首地址返回到上一级函数中,实现上一级函数也能使用Fun函数中p指向的空间
return 0;
}
函数指针:
函数名并不是函数地址的代表,这种误解与数组名就是指针一样犯了相同的错误。函数名是函数实体的代表,不是地址的代表,当然,你马上就会有疑问,平时我们不都是把函数名作为函数的地址吗?是的,我可以告诉你,函数名可以作为函数的地址,但是,绝大多数人都忽略了一个条件,从函数到指针的隐式转换是函数名在表达式中的行为,就是说,这个转换仅在表达式中才会发生,这仅是函数名众多性质中的一个,而非本质,函数名的本质就是函数实体的代表。
到了C++,由于C++规定,非静态成员函数的左值不可获得,因此非静态成员函数不存在隐式左值转换,即不存在像常规函数那样的从函数到指针的隐式转换,所以必须在非静态成员函数前使用&操作符才能获得地址
函数指针指向的函数:需要确定好指向的函数的返回值和形参的类型和形参的个数,在定义函数指针变量的时候确定好
int Fun(int a)
{}
int (* fun_p)(int) = Fun;
//int:表示这个指针变量指向的函数的返回值类型是int
fun_p = Fun;可以或者fun_p = &Fun;
(fun_p)(1); //通过函数的地址解引用得到函数的名字,在通过函数的名字进行函数的调用(通过函数的地址调用函数,称之为回调函数)*
fun_p(1);
数组指针
数组指针定义方法
elemtype (array)[];*
数组指针形参定义方法
elemtype (*)[]
函数指针变量的基本作用:存放函数的地址,解引用实现函数的调用,通过函数的地址调用函数称为回调函数
const int *p = &a; //修饰 *p,*p能不更改 *p=11;
int const *p = &a; //修饰 *p,*p能不更改 *p=11;
int * const p = &a;//修饰p,p不能更改
普通变量的地址:int a = 10; &a表示a变量的首地址,也就是其占4个字节的第一个字节的地址。&a+1跳4个字节
对指针变量的地址:
int *p = &a; &p:指针变量p的地址,也就是p本身占8个字节,&p是这个8个字节中第一个字节的地址。
如果&p是指针变量p的首地址,那我想得到指针变量p的尾地址怎么办?(拓展)
指针变量的地址+1跳 :32位系统:4 字节 64位系统:8字节
如果&a是变量a的首地址,那我想得到变量a的尾地址怎么办?(拓展)
((char *)&a)+3
因为一个指针变量在64位系统是占8个字节,所以&p只是这个8个字节中的第一个字节地址。 int a; &a
指针变量的地址
基础:验证不管是什么指针变量,64位系统是不是他的地址+1,就是跳8个字节。 (课后消化练习)
因为不管是什么指针变量,他的地址+1,先理解成解引用之后计算改数据的大小
int a = 10;
int *p = &a;p的地址上面存放了a的地址。
int * p ;//一级指针变量 &p:一级指针变量的地址
定义: int q = NULL; 定义一个二级指针变量
赋值: q = &p; //让二级指针变量q存放一级指针p的地址
初始化:int q = &p;//二级指针变量,专门存放一级指针变量的地址
除了定义和初始化之外,*的作用就是解引用。
int **q = (int *) malloc(sizeof(int ));//申请一个指针变量的空间也就是8个字节,把首字节的地址存放进q里面
int * p = (int )malloc(sizeof(int)); //申请一个整型变量的空间也就是4个字节的空间,把首字节的地址存放进p里面*
*p = 4;
int **q = (int *) malloc(sizeof(int ));//申请一个指针变量的空间也就是8个字节,把首字节的地址存放进q里面
q存放的是一个一级指针变量的地址
q就是得到这个一级指针变量,一级指针变量只能存放普通变量的地址,所以q只能存放普通变量的地址
int a;
*q = p;
#include <stdio.h>
#include <stdlib.h>
int main()
{
int * p = (int )malloc(sizeof(int)4); //申请4个整型变量空间
if(p == (int *)NULL)
{
printf("malloc failed!\n");
return -1;
}
*p = 4;
int ** q = (int **)malloc(sizeof(int )4);//申请4个一级指针变量的空间
if(q == (int **)NULL)
{
printf("malloc failed!\n");
return -1;
}
*q = p; //*q表示第一个一级指针变量 存放 整形空间的首地址 但是二级指针q修改不了指针p的值q只是将它指向的地址的数据变成了p;q并没有指向p,q = &p是可以改变p的值的
printf("%d---%d\n",p,**q);*
free(p);
free(q);
return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int ** heap_p = (int **)malloc(sizeof(int )4); //申请了32个字节的空间
if(heap_p == (int **)NULL)
{
printf("申请堆空间失败!\n");
return -1;
}
memset(heap_p,0,sizeof(int )4);
int a = 10,b = 121,c=111,d=999;
*heap_p = &a;
(heap_p+1) = &b; //heap_p+1 加了八个字节*
(heap_p+2) = &c; //*heap_p+1 加了四个字节*
(heap_p+3) = &d; //三级指针加一加八个字节 解引用再加一也是加八个字节*
printf("%d--%d--%d--%d\n",heap_p,(heap_p+1),(heap_p+2),(heap_p+3));
free(heap_p);
return 0;
}
返回值:返回一个二级指针
#include <stdio.h>
#include <stdlib.h>
int ** double_pointer_test();
int ** double_pointer_test()
{
//给一个二级指针变量申请指向的堆空间 其实就是一个一级指针变量的空间
int ** p = (int *)malloc(sizeof(int ));
if(p == (int **)NULL)
{
printf("申请堆空间失败!\n");
return (int **)-1;
}
//给一个一级指针变量申请指向的堆空间,其实就是一个普通数据4个字节的空间
int * q = (int )malloc(sizeof(int));*
if(q == (int *)NULL)
{
printf("申请堆空间失败!\n");
return (int **)-1;
}
*p = q; //q一级指针给一级指针变量 *p是一级指针变量 ,p存放的是一级指针变量的地址
*q = 4567; //4567不会被栈空间回收因为他是常量他在常量区
printf("%d---%d\n",**p,q);*
scanf("%d",q);
printf("%d---%d\n",**p,q);*
scanf("%d",p);*
printf("%d---%d\n",**p,q);*
return p;
}
int num
*p = # //p=&num 只有在定义的时候 int *p = # p才等于&num
二维数组
整形 二维数组的定义:定义的时候,两个中括号里面一定是常量或者常量表达式
int array[行] [列];
行和列这两个数,哪一个一定要在定义的时候确定好? 列
有以下的二维数组:int array[3] [5];*
请问:
array:二维数组的名字,可以表示其第一个一维数组的地址。即 &array[0]
&array: 二维数组本身的地址
array[0]:二维数组中的首元素,即第一个一维数组
& array[0] :二维数组中的首元素的地址,即第一个一维数组的地址
array+1:第一个一维数组的地址+1,即第二个一维数组的地址
array[0]+1 第一个一维数组的首元素的地址+1即第一个一维数组的第二个元素的地址 &array[0] [1]
&array[0] [0]+1 第一个一维数组的首元素的地址+1即第一个一维数组的第二个元素的地址 &array[0] [1]
main()
{ int a[3] [3],* p,i;
p=&a[0] [0];
for(i=0;i<9;i++) p[i]=i+1;
//p存放的是数组中9个元素中第一个元素的地址,比喻为某一个一维数组的首元素地址可以表示这个一维数组的名字 //二维数组名字赋值给指针会退化为一维数组
有一个整形数组了存放了 n个 0 和m个1(n可以和m相同),请你不使用条件判断(包括但不限于if)的前提下统计数组中0和1的个数。
譬如:int a[] = {0,1,0,1,1,1,1,0,0,0,0,1,1,1}
*#include <stdio.h>*
int main()
{
int a[] = {0,1,0,1,1,1,1,0,0,0,0,1,1,1}*;*
int b[2]= {0,0}*;//b[0]计算0的个数 b[1]计算1的个数*
for(int *lp*=0*; lp<sizeof(a)/sizeof(int); lp++)*
{
b[a[lp]]++*;*
}
printf("0的个数:%d ---- 1的个数:%d\n",b[0],b[1])*;*
return 0*;*
}
注意:除了初始化可以不填行之外,数组的行必须填!
int array[][3] = {{1,2},4,5,6,7};
第一个一维数组: 1,2,0,
第二个一维数组: 4,5,6,
第三个一维数组: 7,0,0,
int array[2][3] = {{1,2},3,{4,5}};
第一个一维数组: 1,2,0
第二个一维数组: 3,4,0
数据也是按顺序赋值的,但是,这种写法破坏了自动计算一维数组的元素
-----------------------------研究二维数组和指针的关系-----------------------------------------------------
二维数组首元素的地址: array或者是&array[0],
第一个一维数组的首元素的地址: &array[0][0]或者array[0]
因为:
int array[2];那么array[0]就是第一个整形变量,所以&array[0]就是这个一维数组的首元素地址
int array[2][4],那么 array[0]是二维数组的首元素,所以 &array[0]就是这个二维数组的首元素的地址。
一维整形数组是用来存放整形变量的,所以一维数组的首元素地址就是第一个整形变量的地址。
二维数组是用来存放一维数组的,所以二维数组的首元素地址就是第一个一维数组的地址。返过来,第一个一维数组的地址表示二维数组的名字
int array[2] [4]
printf("%d\n",(int)((char *)(&array[0] [0]+1) - (char *)(&array[0] [0])))*; // 4*
printf("%d\n",(int)((char *)(array[0]+1) - (char *)(array[0])))*; // 4*
printf("%d\n",(int)((char *)(&array[0]+1) - (char *)(&array[0])))*; // 16 第一个一维数组的地址+1*
printf("%d\n",(int)((char *)(array+1) - (char *)(array)))*;//16*
printf("%d\n",(int)((char *)(&array+1) - (char *)(&array)))*;//二维数组的地址+1 32*
二维数组:int a[2] [3]表示存放有两个一维数组分别是:a[0] a[1],两个一维数组的长度都是3,第一个一维数组的第一个元素是:array[0] [0]
array是二维数组的名字表示首元素的地址,其首元素就是第一个一维数组的地址 array等于 &array[0] &(&array[0] [0])等于&array[0]
第一步:先算array :array表示二维数组的名字等于其首元素的地址,&array[0]
第二步:*array等于 *(&array[0]) 等于 array[0]
第三步:算(*array)+lp 等于 array[0] + lp,array[0]等于第一个一维数组的名字所以可以表示第一个一维数组的首元素地址 &array[0] [0]
*(&array[0] [0]+lp)
二维数组两次解引用可以得到数组第一个元素 **array == array[0][0]
int array[2] [3] = {{1,2,3},{4,5,6}};
3的元素数据的访问:
array[0] [2]:略
(*array)[2]:array表示第一个一维数组的地址,解引用得到第一个一维数组,然后再写[2]访问3
*(array[0]+2):array[0]表示第一个一维数组表示其第一个元素的地址,array[0] [0]的地址,+2就得到arrat[0] [2]的地址,最后在解引用得到array[0] [2]
(array+2):array表示第一个一维数组的地址,解引用得到第一个一维数组,第一个一维数组又可以表示其首元素的地址即array[0] [0]的地址,+2就得到array[0] [2]的地址
switch case
switch 只能整型字符型
switch('a')
case'b':case'e':case'a': printf("apple");break;
default:printf("f");
输入a会输出apple 会穿透
输入输出缓冲区
缓冲区Buffer
内存中用于暂时保存输入输出数据的取余(为了减少IO系统调用次数,提高效率)
分为 完全缓冲,行缓冲,无缓冲
完全缓冲:
整个缓冲区被填满才刷新缓存(c语言中,若是没有自行设置,缓冲区的大小默认由stdio.h中的宏BUFSIZ定义,为512个字节)
行缓冲:
如:printf() stdout
一行结束即输入换行符'\n'时刷新缓存
无缓冲
如:stderr
直接输入输出 不需要进缓冲区
stdin,stdout,stderr
我们在写C程序时经常遇到printf(),fprintf(),perror(),这些东西到底有什么作用。说到这不得不提及stdin,stdout,stderr。想想,我们在用C去写文件时的操作,File *fp=fopen(),这个fp就是我们向系统申请的,相当于一通往文件的通道。
其实,stdin,stdout,stderr就是这个fp,不过他是随着计算机系统的开启默认打开的,其中0就是stdin,表示输入流,指从键盘输入,1代表stdout,2代表stderr,1,2默认是显示器。printf()其实就是向stdout中输出,等同于fprintf(stdout,“ * * * ”),perror()其实就是向stderr中输出,相当于fprintf(stderr,“***”),那到底stdout,和stderr有什么区别和作用呢?
我们在写程序时用printf()是为了我们能监控我们的程序运行状况,或者是说debug,如果我们的程序是一直运行,不停下来,我们不可能时刻盯着屏幕去看程序输出,这时我们就可以用文件重定向。将输出到一文件中,我们以后就可以看这文件就行。举个例子,test.c
-
#include
-
int main()
-
{
-
printf("stdout Helo World!!\n");
-
return 0;
-
}
编译过后,我们./test > test.txt(默认是将stdout里的内容重定向到文件中),这样就把test程序输出的内容输出到test.txt文件中。还有一种更明晰的写法./test 1>test.txt,这里的1就代表stdout。说到这你应该知道stderr该怎样处理了。再举个例子test.c:
-
include<stdio.h>
- int main()
- {
- printf("Stdout Helo World!!\n");
- fprintf(stdout,"Stdout Hello World!!\n");
- perror("Stderr Hello World!!\n");
- fprintf(stderr,"Stderr Hello World!!\n");
- return 0;
- }
编译过后,./test,屏幕上是四条输出,如果./test > test.ext ,结果是屏幕上输出两条Stderr Hello World!!,Stdout Helo World!!在文件test.txt中,基于上面说的很容易理解现在的结果,于是我们可以随便处理我们想要的输出,例如:
./test 1>testout.txt 2>testerr.txt,我们将stdout输出到文件testout.txt中,将stderr输出到testerr.txt文件中;
./test 1>testout.txt ,将stdout输出到文件testout.txt 中,stderr输出到屏幕上;
./test 2>testerr.txt,将stderr输出到文件testerr.txt中,stdout输出到屏幕上;
./test > test.txt 2>&1,这是将stdout和stderr重定向到同一文件test.txt文件中。
如果我们不想看到输出内容,既不想在屏幕上看见,也不想重定向到文件中,别担心,万能的Linux有解决办法,./test > /dev/zero 2>&1,这样就看不到任何输出了。
Note:stderr,和stdout还有重要一点区别,stderr是没有缓冲的,它立即输出,而stdout默认是行缓冲,也就是它遇到‘\n’,才向外输出内容,如果你想stdout也实时输出内容,那就在输出语句后加上fflush(stdout),这样就能达到实时输出的效果。
printf和scanf
printf()和scanf()搭配就算不换行也可以输出
在标准C中,fflush 函数的作用是刷新输出流的缓冲区,并将缓冲区中的数据立即写入到对应的输出设备中。然而,对输入流(如stdin)使用fflush 是未定义行为(undefined behavior)。
这意味着标准C并没有定义fflush 在输入流上的具体行为,因此不同的编译器或平台可能会有不同的处理方式。
通常情况下,fflush(stdin) 不会清空输入缓冲区的内容,因为标准C并没有规定对输入流使用fflush 的行为。因此,不建议依赖fflush(stdin) 来清空输入缓冲区,而是使用其他方法来实现清空输入缓冲区的目的,
比如使用循环读取或getchar 函数逐个读取字符直至换行符或文件结束符。
scanf()返回值是接收的有效元素的个数,(有效元素:输入跟接收类型匹配)
缓冲区是队列(FIFO)
scanf("%s",str);
遇到空格,回车符时结束本次输入
不可回收空格,回车
会忽略行开头的所有空格和回车
输入n个字符,数组大小至少为n //输入abc\n 会变成abc 回车符会继续保留在缓冲区
两次使用scanf("%s",str); 是不会出错的因为scanf("%s",str);会忽略开头的换行和空格这样会在缓冲区留下两个换行。
getchar()和scanf("%c",&c);
在遇到回车符'\n'时,结束本次输入,仅取输入中的第一个字符
接收空格,回车(不会将'\n'替换为其他) 不会忽略行开头的空格
此外getchar函数返回整型ASCII码值
两次使用getchar() 和scanf("%c",&c); 会出错。
gets(str)
遇到回车符时结束本次输入
接收空格回车,会将\n替换成\0
输入n个字符,数组大小至少为n+1 //输入abc\n 会变成abc\0
getchar()函数
int getchar(void)
C 库函数 int getchar(void) 从标准输入 stdin 获取一个字符(一个无符号字符)。这等同于 getc 带有 stdin 作为参数
返回值
该函数以无符号 char 强制转换为 int 的形式返回读取的字符,如果到达文件末尾或发生读错误,则返回 EOF。
putchar()函数
int putchar(int char)
描述
C 库函数 int putchar(int char) 把参数 char 指定的字符(一个无符号字符)写入到标准输出 stdout 中。
返回值
该函数以无符号 char 强制转换为 int 的形式返回写入的字符,如果发生错误则返回 EOF
gets()函数
描述
C 库函数 char *gets(char *str) 从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。当读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。
char *gets(char *str)
参数
str -- 这是指向一个字符数组的指针,该数组存储了 C 字符串。
返回值
如果成功,该函数返回 str。如果发生错误或者到达文件末尾时还未读取任何字符,则返回 NULL。
include <stdio.h>
int main()
{
char str[50];
printf("请输入一个字符串:");
gets(str);
printf("您输入的字符串是:%s", str);
return(0);
} //结果:请输入一个字符串:runoob
您输入的字符串是:runoob
a = (-1)&&(-1)
a == 1
请看下面的代码(代码一):
include <stdio.h>
include <stdlib.h>
int main(){
int a;
char c;
scanf("%d", &a);
c = getchar();
printf("a = %d, c = %c \n", a, c);
return 0;
}
运行结果:
123abc↙
a = 123, c = a
将上面的代码进行更改(代码二):
include <stdio.h>
include <stdlib.h>
int main(){
int a;
char c;
scanf("%d", &a);
fflush(stdin);
c = getchar();
printf("a = %d, c = %c \n", a, c);
return 0;
}
运行结果:
123abc↙
xyz↙
a = 123, c = x
对比上面的代码,代码一没有清空输入缓冲区,回车时,将123赋值给a,缓冲区剩下abc,接着执行getchar(),发现缓冲区有内容,就无需等待用户输入,直接读取了,
将 'a' 赋给 c。代码二执行到fflush(),清空缓冲区,getchar()发现缓冲区没有内容,需要等待用户输入,所以必须输入两次。
【实例】把输入流与一个缓冲区关联,然后清空缓冲区。
include <stdio.h>
char inbuf[BUFSIZ];
int main(void)
{
char a[100];
setbuf(stdin, inbuf);
printf("input a string =");
scanf("%s",a);
/往缓冲区写入数据 /
puts(inbuf);
if(0 ==fflush(inbuf)) /清空文件缓冲区/
{
puts(inbuf);
}
puts(inbuf);
return 0;
}
运行结果:
input a string = qwe
qwe
程序先把缓冲区与输入流关联,这样输入的内容就在缓冲区中是可见的。提示用户输入一个字符串,用户输入后, 该字符串会在缓冲区中保存,这时我们可以使用puts函数输出一 遍,然后使用fflush函数清空缓冲区,再次输出结果为空。
printf默认是___行缓冲机制__输出的。遇到换行符才输出。
加\n就会正常输出,
如果不加\n:
要不就等到缓存区满了才输出
要不就等到程序退出的时候才输出,因为程序退出的时候会自动刷新标准输出缓存区
要不和scanf搭配也可以输出
自己刷新缓存区(fflush
fflush(stdin):
作用:清理标准输入流,把多余的未被保存的数据丢掉。。
如:
int main()
{
int num;
char str[10];
cin>>num;
cout<<num<<endl;
cin>>str;
cout<<str<<endl;
return 0;
}
从stdin获得一个整数存入num,接着立马打印出来;从stdin获得一个字符串存入str,也立马打印出来。但是下面这种可能需要特别考虑:在首行输入了两个整数,在cin>>num之
后,stdin缓冲还有一个整数没被读取。接下来,不等输入字符串,就直接把上面多出来的数字存入到str中去并打印。某种程度上这是操作不规范造成的,但是程序应该要有健壮
性,程序员应该提前预防这种不规范的操作。可以在程序界面上提示“请输入1个整数”,甚至有时候不厌其烦的强调和警告也必要。当然,本例为求简单,并不在UI友好方面做文
章。这时,可以在cin>>str语句前插入fflush(stdin),如此一来就可以清空标准输入缓冲里多余的数据。
fflush(stdout):
对标准输出流的清理,但是它并不是把数据丢掉,而是及时地打印数据到屏幕上。标准输出是以行为单位进行的,也即碰到\n才打印数据到屏幕。这就可能造成延时,但是Windows
平台上,似乎并看不出差别来。也即MSFT已经将stdout的输出改成及时生效了。
fflush函数被广泛使用在多线程、网络编程的消息处理中。
fflush(stdout):清空输出缓冲区,并把缓冲区内容输出。
我们在写C程序时经常遇到printf(),fprintf(),perror(),这些东西到底有什么作用。说到这不得不提及stdin,stdout,stderr。想想,我们在用C去写文件时的操作,File *fp=fopen(),这个fp就是我们向系统申请的,相当于一通往文件的通道。
其实,stdin,stdout,stderr就是这个fp,不过他是随着计算机系统的开启默认打开的,其中0就是stdin,表示输入流,指从键盘输入,1代表stdout,2代表stderr,1,2默认是显示器。printf()其实就是向stdout中输出,等同于fprintf(stdout,“*”),perror()其实就是向stderr中输出,相当于fprintf(stderr,“”),那到底stdout,和stderr有什么区别和作用呢?
我们在写程序时用printf()是为了我们能监控我们的程序运行状况,或者是说debug,如果我们的程序是一直运行,不停下来,我们不可能时刻盯着屏幕去看程序输出,这时我们就可以用文件重定向。将输出到一文件中,我们以后就可以看这文件就行。
fprintf(stdout/stderr)
键盘输入 --> 缓冲区 --> 阻塞函数(scanf/getchar/gets) --> 输入函数从缓冲区获取字符后,删除从缓冲区接收的字符,
并解除阻塞状态,函数不执行完,函数所在的现成就一直停在这里不动
stderr 无缓冲字节输出到屏幕。
fflush()
fflush()不是标准库文件,经笔者测试,VC6.0完美支持,GCC(GCC4.6.2)不支持。
fflush()用于清空文件缓冲区,如果文件是以写的方式打开的,则把缓冲区内容写入文件。其原型为:
int fflush(FILE* stream);
【参数】stream为文件指针。
【返回值】成功返回0,失败返回EOF,错误代码存于errno 中。指定的流没有缓冲区或者只读打开时也返回0值。
fflush()也可用于标准输入(stdin)和标准输出(stdout),用来清空标准输入输出缓冲区。
stdin 是 standard input 的缩写,即标准输入,一般是指键盘;标准输入缓冲区即是用来暂存从键盘输入的内容的缓冲区。stdout 是 standard output 的缩写,即标准输出,一般是指显示器;标准输出缓冲区即是用来暂存将要显示的内容的缓冲区。
rewind(stdin); //也可以清空输出缓冲区
头文件:#include<stdio.h>
a++和++a
彻底搞清 C/C++中a++与++a的区别:
首先 a++和++a 的定义:看个例子
A: a=5; b=++a; // 相当于a=a+1;b=a; 结果是a=6,b=6
B: a=5; b=a++; // 相当于b=a;a=a+1; 结果是a=6,b=5
所以书上有:
(1) 如果用前缀运算符对一个变量增1(减1),则在将该变量增1(减1)后,用新值在表达式中进行其他的运算。
(2) 如果用后缀运算符对一个变量增1(减1),则用该变量的原值在表达式中进行其他的运算后,再将该变量增1(减1)后。
这里就有个问题,对于后缀运算,”用该变量的原值在表达式中进行其他的运算后“该如何理解,也就是说啥时才算表达式运算完啦。
举个列子:求a的最终值
int a=3;
a+=a+=a++a--;*
计算过程如下:
先算a++*a--
由于a=3
a++等于3 //这里后缀++中的第二步自加运算还没有进行
a--等于3 //这里后缀--中的第二步自减运算还没有进行
所以 a++*a-- 等于9
a+=a++a-- 等于a+=9等于a=a+9=3+9*=12
在算a+=a 等于a=a+12=12+12=24
然后在进行再后缀++中的第二步自加运算和后缀--中的第二步自减运算
即进行 a=a+1;此时a=25;再进行a=a-1;此时a=24 所以最终a=24
说明在一个表达中,无论有多少个等号(赋值号)都必须进行完所有的运算符后,才能进行后缀自加,自减。那么什么时候才能
算本计算单位已经结束。下面某些摘自《C语言深度剖析》
i++ 在遇到每个逗号,分号后,才认为本计算单位已经结束,i 这时候自加。
遇到括号的时候也不自加
*(a++)//先 *a再++
关于逗号表达式与“++”或“--”的连用,还有一个比较好的例子:
int x;
int i = 3;x = (++i, i++, i+10);
问 x的值为多少?i的值为多少?
按照上面的讲解,可以很清楚的知道,逗号表达式中,i 在遇到每个逗号后,认为本计算单位已经结束,i 这时候自加。所以,本例子计算完后,i的值为5,x的值为 15。
关于符号的贪心法
C 语言有这样一个规则:每一个符号应该包含尽可能多的字符。也就是说,编译器将程序分解成符号的方法是,从左到右一个一个字符地读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符串是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串 已不再可能组成一个有意义的符号。这个处理的策略被称为“贪心法” 。需要注意到是,除了字符串与字符常量,符号的中间不能嵌有空白(空格、制表符、换行符等) 。比如:是单个符号,而是两个等号。
按照这个规则可能很轻松的判断 a+++b表达式与 a++ +b一致。
那++i+++i+++i;会被解析成什么样子呢?希望读者好好研究研究。另外还可以考虑一下这个表达式的意思a+++++b?
分析:
++i+++i+++i 由于后缀自增,自减优先级比前缀高(ANSI/ISO C++中),编译器等效理解为++(i++)+(i++)+i,而i++不可以作左值,所以++(i++)错误,故此表达式错误
a+++++b 编译器将其理解为(a++)++ +b, 由于a++不可以作左值,所以(a++)++错误,故此表达式错误
另外特别注意 ++i在C中不是左值,在C++中才是。在C语言中像 int i=0,a=2,b=3;++i=a+b;这个表达式是非法的。
i++在C和C++中都不能作左值。
a++是首先拷贝自己的副本,然后对真值加一。
++a是对真值加一,然后使用真值。
C语言中的“左值”和“右值”
1、左值
左值就是那些可以出现在赋值符号左边的东西,它标识了一个可以存储结果值的地点。
程序在编译时,编译器会为每个变量分配一个地址(左值),这个地址在编译是即可知。
也就是说,左值在编译时即可知,左值标志存储结果的一个地方,也可以理解为左值就是一块空间。
2、右值
右值就是那些可以出现在赋值符号右边的东西,它必须具有一个特定的值。与左值相反,变量中存储的那个值(右值),只有在运行时才可知,且只有要用到变量中存储的值时,编译器才会发出指令从指定的地址读入变量的值,并将它存于寄存器中。
也就是说,右值就是一个数字或一个字面值或一个常量,它并不标识任何位置。
左值(lvalue)和右值(rvalue)最先来源于编译。在C语言中表示位于赋值运算符两侧的两个值,左边的就叫左值,右边的就叫右值。
定义:
左值指的是如果一个表达式可以引用到某一个对象,并且这个对象是一块内存空间且可以被检查和存储,那么这个表达式就可以作为一个左值。
右值指的是引用了一个存储在某个内存地址里的数据。
从上面的两个定义可以看出,左值其实要引用一个对象,而一个对象在我们的程序中又肯定有一个名字或者可以通过一个名字访问到,所以左值又可以归纳为:左值表示程序中必须有一个特定的名字引用到这个值。而右值引用的是地址里的内容,所以右值又可以归纳为:右值表示程序中没有一个特定的名字引用到这个值。
++a的话因为返回结果和运算之后的a一样,所以++a返回的是真实的a,可以被重新赋值,所以可以作为左值。而a++返回的是运算之前的a,而此时a已经+1了,返回的数据其实是过去的a,它是另外复制出来的,而不是真正的a,所以无法被赋值,所以它只能是右值。
所以a++;在执行当中的顺序是,先把a的值复制出来,进行整体运算,然后再a=a+1。
int i=0;
i++=3;(错误)
++i = 3(正确)
c语言文件操作
C语言位操作
在 C 语言中,对 char 类型进行左移操作时,char 类型会被自动提升为 int 类型,然后再进行左移操作。这种行为是由 C 语言的标准规定的,称为整数提升(Integer Promotion)。
整数提升是指当小于 int 的整数类型(如 char、short)参与表达式运算时,会被自动提升为 int 类型,以保证表达式中的所有操作数具有相同的类型。这样可以避免由于不同数据类型之间的混合运算而导致数据丢失或错误的结果。
因此,即使对 char 类型进行左移操作,它会先被提升为 int 类型,然后再进行左移操作。这样做可以确保左移操作不会导致数据截断,同时保证计算结果的正确性和一致性。
在这个情况下,即使 bgr 是一个 char 类型(8位),但是在进行位操作时,bgr 会被隐式地提升为 int 类型。这是因为在 C/C++ 中,位移操作符的操作数通常会根据以下规则进行隐式类型转换:
如果操作数的类型是小于 int 的整型类型(比如 char、short 等),则会先被提升为 int 类型再执行位移操作。
如果操作数的类型是大于或等于 int 的整型类型(比如 int、unsigned int 等),则直接执行位移操作。
因此,在你的表达式中,bgr 在进行位移操作时会被隐式地提升为 int 类型,而不会发生高位被舍弃的情况。每次左移操作都会将 bgr 的值拓展到 int 类型的宽度(32位),然后进行位移并组合成最终的 argb 值。
system函数
system()函数
包含在头文件 “stdlib.h” 中
int system(const char * command)
system(“pause”)可以实现冻结屏幕,便于观察程序的执行结果;
system(“CLS”)可以实现清屏操作。//windows
system("clear")实现linux清屏操作
system(color xx)函数可以改变控制台的前景色和背景,“color xx”中的第一个十六进制数为背景色设置,第二个十六进制数为前景色设置。
注:各颜色对应的数值
0 = 黑色 8 = 灰色
1 = 蓝色 9 = 淡蓝色
2 = 绿色 A = 淡绿色
3 = 湖蓝色 B = 淡浅绿色
4 = 红色 C = 淡红色
5 = 紫色 D = 淡紫色
6 = 黄色 E = 淡黄色
7 = 白色 F = 亮白色
system(“del d:\123.txt”)删除文件
system(“ipconfig”);查看当前网络情况
函数功能
执行 dos(windows系统) 或 shell(Linux/Unix系统) 命令,参数字符串command为命令名。另,在windows系统下参数字符串不区分大小写。
说明:在windows系统中,system函数直接在控制台调用一个command命令。
在Linux/Unix系统中,system函数会调用fork函数产生子进程,由子进程来执行command命令,命令执行完后随即返回原调用的进程。
函数返回值
命令执行成功返回0,执行失败返回-1。
举个栗子
实现关机小程序
#include <stdio.h>
#include<string.h>
#include<stdlib.h>
int main(void)
{
char input[100];
system("shutdown -s -t 60");
//这是Windows系统下的一个关机命令
flag:
printf("你的电脑将在一分钟后关机,如果输入“我是猪”,就会取消关机!\n请输入: ");
scanf("%s",input);
if(0==strcmp(input,"我是猪"))
{
system("shutdown -a");//取消关机命令
}
else
{
goto flag;
}
return 0;
}
一些system()函数的参数及实现的功能
表格里的内容可以通过 system(“HELP”);语句获取。
标签:函数,int,C语言,char,printf,变量,指针 From: https://www.cnblogs.com/zyc666888/p/18216422