【C缺陷与陷阱】----语义“陷阱”

💯💯💯

本篇处理的是有关语义误解的问题:即程序员的本意是希望表示某种事物,而实际表示的却是另外一种事物。在本篇我们假定程序员对词法细节和语法细节的理解没有问题,因此着重讨论语义细节。

  • 导言:
  • ①.指针与数组
    • 1.1数组的两个注意点:
      • 1.1.1确定数组的大小
      • 1.1.2获得指向该数组下标为0的元素的指针。
    • 1.2指针与数组之间的真正关系
      • 1.2.1数组名表示首元素的地址(指针)
      • 1.2.2“二维数组”
  • ②.非指针的数组
  • ③.作为参数的数组声明
    • 3.1一维数组的传参
    • 3.2二维数组的传参
    • 3.3一级指针传参
    • 3.4二级级指针传参
  • ④.避免"举隅法"
  • ⑤.空指针并非空字符串
    • 注意
  • ⑥.边界计算与不对称边界
    • 6.1 死循环问题
      • 6.1.1死循环的原因是什么呢?
      • 6.1.2解决方法
      • 6.1.3总结
    • 6.2 边界问题
      • 6.2.1两个原则和一个编程技巧
    • 6.3 边界访问
  • ⑦.求值顺序
    • 7.1短路求值:&&和||
  • ⑧.整数溢出
  • ⑨.main函数的返回值

导言:

由于一个程序错误可以从不同层面采用不同方式进行考察,而根据程序错误与考察程序的方式之间的相关性,可以将程序错误进行划分为各种陷阱与缺陷:
①.词法“陷阱”
②.语法“陷阱”
③.语义“陷阱”
④.连接问题
⑤.库函数问题
⑥.预处理器问题
⑦.可移植性缺陷
本篇重点讲解语义“陷阱”

①.指针与数组

1.1数组的两个注意点:

1.1.1确定数组的大小

数组的声明必须确定数组的大小是多少

int a[3];/

声明a是一个拥有3个整形的数组

struct arr
{
  int p[4];
  double x;
}b[10];

声明了b是一个拥有10个元素的数组,每个数组元素是个结构体,该结构体中包含了一个拥有4个整形的数组和一个浮点型的变量。

1.1.2获得指向该数组下标为0的元素的指针。

这句话什么意思呢?
其实有关数组的操作,哪怕它们乍看上去是以数组下标进行运算的,实际上都是通过指针进行运算的
换句话说,任何一个数组下标的运算都等同于一个对应的指针运算,所以我们完全可以依据指针行为定义数组下标的行为。

那获得该下标为0的元素的指针,如果给这个指针加1,就能得到指向该数组中下一个元素的指针。
也就是指针+一个整数得到的还是指针,只不过指针的位置发生改变,
注意可不是指针指向的内容发生改变了!

int a[10]={0,1,2,3,4,5,6,7,8,9};
int  *p=a;
p=p+1;
//或者写成p++;

1.2指针与数组之间的真正关系

  1. sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
    sizeof(a)的结果是整个数组a的大小,而不是指向数组a首元素地址的大小。
  2. &数组名,这里的数组名表示整个数组,取出的是整个数组的地址
  3. 除此之外所有的数组名都表示首元素的地址(指针)
    这里重点讲解第三个关系

1.2.1数组名表示首元素的地址(指针)

数组名表示首元素的地址
除了a被用作运算符sizeof的参数和&a的情况,在其他所有的情况中数组名a都代表着指向数组a中下标为0的元素的指针。
也就是数组名a代表着数组a中首元素的地址。

所以我们可以这样理解*a也就是数组a中下标为0的元素的引用。

*a=10;

这句代码也就是把首元素的值改成10. 同理,*(a+1_是数组a中下标为1的元素的引用,以此类推,可以推出,*(a+i)即数组a中下标为i的元素的引用;而这种写法是如此的常用,因为它被简记为a[i];

所以我们可以这样理解数组下标与指针之间的关系

a[0] 等同于*(a+0) //数组名a表示首元素的地址,加0还是首元素,对首元素的解引用,访问的就是首元素,而a[0]就是首元素

a[1]等同于*(a+1) //首元素的地址+1表示指向了第二个元素的地址,解引用就等于访问第二个元素,而a[1]就是第二个元素

a[2]等同于*(a+2)//首元素的的地址+2表示指向了第三个元素的地址,解引用等于访问第三个元素,而a[2]就是第三个元素
…… …… ……
a[i]等同于*(a+i)//就相当于数组a中下标为i的元素的访问    

也正是这一概念让许多C语言新手对数组与指针之间的关系搞得迷迷糊糊的。
实际上,由于a+ii+a的含义是相同的,因此a[i]i[a]也具有相同的含义,但不要写成这样,因为不好理解。

1.2.2“二维数组”

我们可以利用"二维数组"来揭示指针与数组之间的关系

int arr[10][20];

这个代码声明arr是一个数组。该数组是拥有10个数组类型的元素。其中每个元素都是一个拥有20个整形元素的数组(而不是一个拥有20个数组类型的元素,每个元素都是一个拥有10个整形的数组)。

根据我们对一维数组的理解,arr总是被转换成一个指向arr数组的起始元素的的指针。也就是首元素的地址。

这里还是提醒一下,该数组仍然遵循上面的第1条
sizeof(数组名)表示的是整个数组的大小,而这个二维数组的整个大小是10 * 20*sizeof(int)

而这个"二维数组",它实际上是以数组为元素的数组。它是一维数组里面又套了一个一维数组。
在一维数组里面,我们可以比较轻松的利用指针来编写操纵一维数组的程序,但是对于二维数组从记法上的便利性来说采用下标的形式才是更好的方式。
如果我们仅仅使用指针来操纵二维数组,那我们就必须深刻的理解指针相关的知识,不然会常常遇到意想不到的bug。

来我们看一下,下面的声明

int a[10];
int arr[10][20];
int *p;
int i;

想一想arr[3]代表的是什么?
a[3]表示一维数组a中第4个元素。

那arr[3],对于二维数组arr,是什么意思呢?
我读这个二维数组的方式是这个二维数组有10个数组类型。
10个数组元素,每个数组元素有20个整形元素。
所以arr[3],就代表着这10个数组元素的第4个数组元素。

其实我们还可以这样理解:将二维数组第一个[ ]里看成是行数
第二个[ ]看成列数。
那么有该二维数组有10行20列
arr[0]表示第1行的数组元素
arr[1]表示第2行的数组元素
arr[2]表示第3行的数组元素
arr[3]表示第4行的数组元素
arr[4]表示第5行的数组元素
arr[5]表示第6行的数组元素
arr[6]表示第7行的数组元素
arr[7]表示第8行的数组元素
arr[8]表示第9行的数组元素
arr[9]表示第10行的数组元素

那arr[3]表示的是第4行数组元素。

我们知道每一行的数组元素里面,都有着20个整形元素
所以一个arr[3]的大小就是sizeof(arr[3])==20*sizeof(int)

//这段代码表示什么意思?
p=arr[3];

这个语句使得指针p指向了数组arr[3]中下标为0的元素。
为什么是这样呢?
因为你看,arr[3]表示的是数组,该数组里面还有20个元素呢
所以arr[3]表示这个拥有20个整形元素的数组的数组名
数组名代表着什么?
代表着首元素的地址!所以该语句将arr[3]数组的首元素的地址传给了
p。
*arr[3]就是对arr[3]这个数组的首元素的地址的访问了,也就是它那20个元素的首元素。

*(arr[3]+1)这就表示访问第4个数组元素里面的第2个元素 也就是arr[3][1]
*(arr[3]+3)这就表示访问第4个数组元素里面的第3个元素==arr[3][2]
*(arr[3]+4)这就表示访问第4个数组元素里面的第4个元素==arr[3][3]
*(arr[3]+5)这就表示访问第4个数组元素里面的第5个元素==arr[3][4]
…… ……
*(arr[3]+i)这就表示访问第4个数组元素里面的第i个元素==arr[3][i]

这个语句根据前面的类似的道理,还可以写成下面的这样

*(arr[3]+3)这就表示访问第4个数组元素里面的第3个元素==arr[3][2]
进一步写成这样---也就是将arr[3]下标形式写成指针形式
*(*(arr+3)+3)

我们不难发现,用带方括号的下标形式很明显的要比完全用指针来表达方便多了。

不过还有人经常犯错误,写成下面这样

p=arr;

这个语句是非法,因为arr是一个二维数组,即”数组的数组“
arr表示的是首元素的地址对吧
你想一想,arr首元素是个啥?还是个数组呀!
所以arr首元素的地址是数组的地址,使用arr时,会转化为一个指向数组的指针,而p是一个指向整形变量的指针,可不能将一种类型的指针,赋给另一种类型的指针,这是非法的。

很显然,我们需要一个指向数组的指针来保存arr,上一篇博客我已经较为详细的介绍了该如声明一个变量:按照使用的方式来声明
我们需要的是一个指针,该指针是指向一个数组的,该数组的大小是20

int(*ph)[20];
//我们首先构造出(*ph)这个指针
//这个指针指向的是什么类型的呢?--是int [20]类型的

该语句实现的效果就是*ph是一个拥有20个整形元素的数组
所以ph就是一个指向这样的数组的指针。

所以可以这样写

int arr[10][20];
int(*ph)[20];
ph=arr;

ph也就指向数组arr的第一个元素的地址了,也就是数组arr的10个中有着20个元素的数组的元素之一。

利用上面的"二维数组"可以很好的揭示C语言中数组与指针之间独特的关系,从而更清楚的明白理解这两个概念。

②.非指针的数组

1.在C语言中,字符串常量代表的是一块包括字符串中所有字符还有一个’\0’的内存区域的地址。
2.字符串常量是以空字符’\0’作为结束标志。
3.字符串如果不用数组存储,那必须要有指针来存储。并且该指针指向的是字符串首字符的地址。

我们如果想让两个字符串合并成一个字符串

给定一个想法:
先计算出两个字符串长度,计算总长度多少
利用malloc函数,开辟一个大小为总长度的空间
将两个字符拷贝过去

注意事项:
1.如果利用stren计算字符串长度,请记住最后的结果要加上1,因为strlen遇到’\0’就停止。最后并没有将’\0’计算进去。
2.malloc申请的空间可能失败,需要判断
3.malloc申请的空间,在程序结束之前需要释放

③.作为参数的数组声明

在C语言中,我们虽然没有办法将一个数组作为函数参数之间参过去,因为不知道数组有多大,如果超级大,那操作系统可能无法提供足够的空间。
但如果我们使用数组名作为参数,那么数组名会立刻被转化为指向该数组第一个元素的指针。
数组传参和指针传参
写代码时难免要把数组或者指针传给函数,那函数的参数该怎么设计呢?

3.1一维数组的传参

void test(int arr[])//数组传参,数组接收
{}
void test(int arr[10])//跟上面一样
{}
void test(int* arr)//数组传参,指针接收
{}
void test2(int* arr[20])//数组传参,指针接收
{}
void test2(int** arr)//数组传参,指针接收
{}
因为数组名就是首元素的地址,所以数组名传参,可以用数组来接收,也可以用地址来接收,只不过要注意接收的是一级指针还是二级指针。
int main()
{
	int arr[10] = { 0 };
	int* arr2[20] = { 0 };
	test(arr);
	test2(arr2);
	return 0;
}

3.2二维数组的传参

void test(int arr[3][5])//ok?二维数组传参,二维数组接收
{}
void test(int arr[][])//ok?这个可不行喔
{}
void test(int arr[][5])//ok?跟第一个一样

注意点:二维数组传参,函数参数的设计只能省略第一个[ ]的数字。
因为对于一个二维数组,可以不知道到多少行,但不能不知道一行多少个元素。这样才方便运算。

void test(int *arr)//ok?还记得上面说的吗,二维数组的数组名相当于第一行的数组地址所以是类型是数组指针类型的,应该用数组指针来接收
{}
void test(int* arr[5])//ok?不行这是个指针
{}
void test(int (*arr)[5])//ok?这个可以是数组指针,指针指向一个五个整形的数组
{}
void test(int **arr)//ok?不行,这个是二级指针
{}
int main()
{
 int arr[3][5] = {0};
 test(arr);
 }

3.3一级指针传参

//用一级指针来接收
void print(int *p, int sz)
{
 int i = 0;
 for(i=0; i<sz; i++)
 {
 printf("%d\n", *(p+i));
 }
}
int main()
{
 int arr[10] = {1,2,3,4,5,6,7,8,9};
 int *p = arr;
 int sz = sizeof(arr)/sizeof(arr[0]);
 //一级指针p,传给函数
 print(p, sz);
 return 0;
 }

想一想当函数参数为一级指针时,可以接收上面参数呢?

void test1(int * p)
{ }
//test1函数能接收什么参数?
void test2(char* p)
{ }
//test2函数能接收什么参数?

3.4二级级指针传参

void test(int** ptr)
{
 printf("num = %d\n", **ptr); 
}
int main()
{
 int n = 10;
 int*p = &n;
 int **pp = &p;
 test(pp);//pp是二级指针
 test(&p);//p是一级指针,&p就应该用二级指针来接收了
 return 0;
 }

当函数的参数为二级指针的时候,可以接收什么参数?

void  test(char**p)
{

}
int mian()
{
  char c='b';
  char *pc=&c;
  char**pcc=&pc;
  char *arr[10];
  test(&pc);//指针的地址,需要二级指针接收
  test(ppc)//二级指针,二级指针接收
  test(arr);//数组名表示首元素地址,每个元素的类型是char类型的指针,然后再取地址,当然可以用二级指针接收。
}

④.避免"举隅法"

什么叫"举隅法",就是以整体代表部分,或者以部分代替整体。
在C语言,我们会遇到常见的"陷阱":混淆指针与指针所指向的数据。
对于字符串的情形,我们更是经常犯错误。
比如:

char *p,*q;
p="abc";

我们可能一开始初学时认为,上面的赋值语句将字符串"abc"赋给了p,
然而实际上并不是这样,要记住字符串的不同之处。
实际上p的值是一个指向由’a’,‘b’,‘c’,'\0’四个字符组成的数组的起始字符的指针。

如果我们执行下面的语句

q=p;

让p和q同时指向用一块空间,但这个赋值语句并没有将p的数据 赋值给q
只是改变了p原来的指向而已。
所以我们要记住,复制指针并不同时复制指针所指向的数据。

⑤.空指针并非空字符串

在C语言中将一个整数转化为一个指针,最后得到的结果都取决与具体的C编译器实现,。但有个特殊情况,那就是常数0,编译器保证由0转化而来的指针,不等于任何有效的指针。
所以0通常被写成NULL

#define NULL 0;

无论是用常数0还是用符号NULL,效果是一样的。
但是要记住的重点是:

注意

当常数0被转化为指针使用时,这个给指针就不能再被访问,解引用了。
也就是当使用NULL时,就不能再去企图访问这个指针指向的内容了,一旦访问,就会造成非法。
比如下面这样:

char *p=NULL;
char *l="abc";
if(strcmp(l,p)==0);

这样就非法访问了,因为库函数,strcmo的使用会查看它的指针参数所指向内存中的内容的操作,因为p为空指针,如果访问它就会非法。

printf(p);
printf("%s",p);

以上两个写法也是未定义的。在不同的环境会出现不同的效果。

⑥.边界计算与不对称边界

a[10];在C语言中,数组下标的范围,是0-9,大家应该都不陌生。
而在有些语言中数组下标是1-11,有的是0-10等等。
今天我们就来探究一下C语言这样设计的动机是什么。

6.1 死循环问题

我们先来看一段代码:

int main()
{
	int i;
	int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
	for (i = 0; i < 12; i++)
	{
		printf("xiao tao\n");
		arr[i] = 0;
	}
	return 0;
}

这段代码的本意是要将数组a中所有的元素置为0,但却产生了一个出乎意料的"副作用",在for语句中比较部分本来是i<10,却写成了12,因此实际上并不存在的a[10],a[11]都被设置为0,也就是内存中在在数组后面的数据被设置为0。然后最终该程序变成了死循环,下面将会详细的讲解为什么会出现这样的情况,但该部分不是本篇重点,如需知道下面的知识,请跳过。

你觉得该代码有什么问题呢?

1.越界访问
2.死循环

第一个问题越界访问非常容易看出来,数组arr只有10个元素,下标从0-9,而循环12次,肯定越界访问了
每次都会打印一个xiao tao,并且把对应的arr[i]置为0。
所以最后答案应该就是打印了12次xiao tao,然后越界访问出现错误,使arr[10]=0,arr[11]=0了
但最后答案却不是这样。
答案变成了死循环了,死循环打印xiao tao

在这里插入图片描述

6.1.1死循环的原因是什么呢?

这里涉及有关栈空间的知识:

1.
内存空间分为栈区,堆区和静态区

栈区一般存放局部变量,函数参数,函数返回值等
堆区是用来分配动态开辟的空间的
静态区是存放全局变量,static修饰的静态变量等

在这里插入图片描述


2.局部变量是在栈区存放的

3.栈区的使用习惯:
先使用高地址处的空间
再使用低地址处的空间
4.数组的地址随着下标的增长,地址是由低到高变化的

在这里插入图片描述
注意:
该测试是在VS2019 X86环境下进行,其他环境可能不一样,结果也就不一样,不能一概而论。
在变量i与数组之间一定有两个整形空间吗?
答案:不一定。
在VS2019 X86环境下,变量i与数组之间确实空了两个整形变量空间大小。
而在VC6.0环境下,变量i与数组之间没有剩余空间。
在gcc环境下,变量i与数组之间有一个整形空间大小
在这里插入图片描述

6.1.2解决方法

可能有的人会这样想将变量i定义在数组的下面这样就不会发生死循环了
我们从栈空间使用方面来看,这样当然可以避免死循环,但是难道我们以后写数组都要把i写在数组的后面吗?
这样只能解决当前的问题,而不能解决根本。不过现在的编译器大多数会自己修改这个死循环问题,比如将变量的i的地址放在数组的下面,在release版本就是这样进行优化不会死循环。

我们一般可能想不到这样本质原因,但我们可以通过调试来解决这个问题
当让i不断的++,当i等于10时将arr[10]置0,当i等于11时将arr[11]置0,然后我们通过调试监视发现arr[12]与i的值相等,这时我们就要想到为什么会死循环了,arr[12]的地址就等于i的地址,将arr[12]修改成0,就等于将i改为0了。
在这里插入图片描述
将arr[12]置0,发现i也变成0了
在这里插入图片描述

6.1.3总结

其实这道题是在特殊环境下才能实现的,但我们还是要注意的是其中的知识点:
1.栈区的使用习惯
先使用高地址的空间
再使用低地址的空间

2.数组随着下标的增长,地址不断增大

6.2 边界问题

在所有常见的程序设计错误中,最难于察觉的一类是"栏杆错误",也常被称为"差一错误",其实总结起来都是边界问题。
比如100米长的围栏每隔10米就需要一根支撑用的栏杆,那一共需要多少根栏杆呢?
”显而易见“答案是10,不就是100处以10嘛,得到结果10,需要10根栏杆。
这个答案是错误的,正确答案是11根。

仔细想一想:一开始支持这10米长的围栏需要两个跟,两端各一根,然后从第二根开始往后计数,每个10米要一根,最后加起来就是10+1
这个1是一开始没有计算的那根。

6.2.1两个原则和一个编程技巧

为了避免”栏杆错误“,总结以下两个原则:

  • 首先考虑最简单情况下的特例,然后将得到的结果往外推。
  • 仔细计算边界,绝不能掉以轻心

一个编程技巧:

  • 用一个入界点和第一个出界点来表示一个数组范围。
    比方说:整数x满足x>=16且x<=48;
    求满足整数x的个数是多少呢?
    我们可以利用编程技巧将它转变以下
    写成x>=16且x<49;
    这里的下界就是”入界点“,即包含在取值范围之中,而上界是”出结点“,即不包含在取值范围内。
    虽然这样形成了两个不对称(左边带有等于,右边不带有等于),但编程效果是极佳的,因为最后的答案就是上界-下界。

对于像C这样的数组下标从0开始的语言,不对称边界给程序设计代来了许多便利。
为什么呢?
因为这样数组的上界恰是数组元素的个数!
因此如果我们要在C语言中定义一个拥有10各元素的数组,那么0就是数组下标的第一个入界点,而10就是下标中的第一个出界点。

我们经常这样写

int a[10],i;
for(i=0;i<10;i++)
{
a[i]=10;
}

而不是这样写:

int a[10],i;
for(i=1;i<=10;i++)
{
a[i]=10;
}

是有道理的。所以我们在控制循环时,设置变量的范围是通常是设置为不对称形式,而不设置称对称形式。

6.3 边界访问

ANSIC标准明确允许这样的用法:
数组中实际不存在的”溢界“元素的地址位于数组所占内存之后,这个地址是可以用于进行赋值和比较,但是如果要引用该元素,那就是非法的了。
什么意思呢?
就是数组最后一个元素的后面那块空间地址是允许拿来进行赋值和比较的,但不允许访问。
该准则与上面的”不对称边界“原则是一致的,空间上也形成不对称,但是要记住不能访问!!!.一旦访问就是非法访问了。

⑦.求值顺序

上篇博客我写了关系运算符优先级之间的问题,但本篇讲的是求值顺序,与它并不相同。

C语言只有4各运算符(&& 和|| 和?:;和,)存在着规定的求值顺序。其他运算符对其操作数求值的顺序是未定义的。特别的,赋值运算符并不保证任何求值顺序

1.条件运算符?:;有三个操作数:在a?b:c中,操作数a首先被求值,然后根据a的值再去求操作数b或c的值。
2.而逗号运算符,首先对左侧操作数求值,然后该值被”丢弃“,再对右侧操作数求值

7.1短路求值:&&和||

逻辑与&&逻辑或||存在着短路求值
什么叫短路求值呢?就是首先对左侧操作数求值,只有当需要时才会对右侧操作数进行求值。

3.逻辑与&&: 表达式1&&表达式2 表达式1为假则右边不再进行。
4.逻辑或|| : 表达式1||表达式2 表达式1为真则右边不再进行。

int main()
{
    int i = 0, a = 0, b = 2, c = 3, d = 4;
    i = a++ && ++b && d++;
    printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
    return 0;
}
因为 i = a++ && ++b && d++;   a++是后置++,先使用后++,所以a=0,先使用与&&进行配对,然后是假,
所以后面++b, d++都不再进行,但a++,这个还是进行的,所以a用完后还要给a+1,
所以a=1,b=2,c=3,d=4
int main()
{
    int i = 0, a = 0, b = 2, c = 3, d = 4;
    i = a++||++b||d++;
    printf("a = %d\n b = %d\n c = %d\nd = %d\n", a, b, c, d);
    return 0;
}
这里a++(a=0,先使用与||配对为假,再+1++b,b自增为3||配对为真,后面的d++不再进行了所以
a=1,b=3,c=3,d=4;

⑧.整数溢出

C语言中存在着两类整数算术运算,有符号运算与无符号运算。在无符号算术中,没有所谓的”溢出“一说:所有的无符号数运算都是以2的n次方为模,这里的n是结果中的位数。
如果算术运算符中一位是有符号,一个是无符号的,有符号的会先转化成无符号数,然后进行运算。
当两个操作数都是有符号数时,才可能发现”溢出“,而且溢出的结果是未定义的。当一个运算的结果发生”溢出“时,做出任何假设都是不安全的。
那如何进行检查两个操作数进行运算时是否”溢出“呢?

一种正确的方式是将a和b都强制转化为无符号整数:

if( (unsigned)a+(unsigned)b>INT_MAX )
exit(1);

这里的INT_MAX是表示最大整数值。

不需要用到无符号算术运算的另一种可行方法是:

if(a>INT_MAX-b)
exit(1);

⑨.main函数的返回值


main()
{
}

函数main什么都没写,与其他任何函数一样,如果没有显示声明返回类型,那么函数返回类型就默认为整形。但是这个程序也没有给出任何返回值。

按理说,这样不会造成什么危害。一个返回值为整形的函数,如果返回失败,实际上是返回了某个”垃圾“整数。只要不用到该整数,就问题不大。

但严格来说,大多数C语言实现是通过函数main的返回值来告知操作系统该函数的执行是否成功。
典型的处理就是main函数返回值为0表示执行成功,返回非0表示执行失败。

#include <stdio.h>
int main()
{
   printf("hello world\n");
	return 0;
}

在这里插入图片描述

#include <stdio.h>
int main()
{
	int arr[10], i;
	for (i = 0; i < 11; i++)
	{
		arr[i] = 0;
	}
	return 0;
}

在这里插入图片描述
如果一个程序的main函数并不返回任何值,那么有可能看上去执行失败。
所以即使是最简单的C程序也应该像这样编写代码:

#include <stdio.h>
int main()
{
   printf("hello world\n");
	return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/312.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

SignalR+WebRTC技术实现音视频即时通讯功能

一、建立信令服务器 1、后台项目中新建一个对应的集线器类&#xff0c;取名VideoHub&#xff0c;并继承Hub类&#xff0c;Hub是SignalR的一个组件&#xff0c;它使用RPC接收从客户端发送来的消息&#xff0c;也能把消息发送给客户端。 2、VideoHub中定义一个静态的Concurrent…

java-正装照换底色小demo-技术分享

文章目录前言java-正装照换底色小demo-技术分享01 实现思路02 效果02::01 原图:02::02 执行单元测试:02::03 效果:03 编码实现前言 如果您觉得有用的话&#xff0c;记得给博主点个赞&#xff0c;评论&#xff0c;收藏一键三连啊&#xff0c;写作不易啊^ _ ^。   而且听说点赞…

js逆向爬取某音乐网站某歌手的歌曲

js逆向爬取某音乐网站某歌手的歌曲一、分析网站1、案例介绍2、寻找列表页Ajax入口&#xff08;1&#xff09;页面展示图。&#xff08;2&#xff09;寻找部分歌曲信息Ajax的token。&#xff08;3&#xff09;寻找歌曲链接&#xff08;4&#xff09;获取歌曲名称和id信息3、寻找…

XXE漏洞复现

目录XML基础概念XML数据格式DTD基础定义DTD作用分类DTD实体实体的分类DTD元素XXE漏洞介绍实操如何探测xxe漏洞XML基础 概念 什么是XML 是一种可扩展标记语言 (Extensible Markup Language, XML) &#xff0c;标准通用标记语言的子集&#xff0c;可以用来标记数据、定义数据类型…

30个题型+代码(冲刺2023蓝桥杯)(中)

2023.3.13~4.13持续更新 目录 &#x1f34e;注意 &#x1f33c;前言 &#x1f33c;十&#xff0c;KMP&#xff08;留坑&#xff09; &#x1f33c;十一&#xff0c;Trie&#xff08;留坑&#xff09; &#x1f33c;十二&#xff0c;BFS &#x1f44a;(一)1562. 微博转发…

OpenAI 发布GPT-4——全网抢先体验

OpenAI 发布GPT-4 最近 OpenAI 犹如开挂一般&#xff0c;上周才刚刚推出GPT-3.5-Turbo API&#xff0c;今天凌晨再次祭出GPT-4这个目前最先进的多模态预训练大模型。与上一代GPT3.5相比&#xff0c;GPT-4最大的飞跃是增加了识图能力&#xff0c;并且回答准确性也得到显著提高。…

写给20、21级学生的话

写给20、21级学生的话前言一、关于招聘变招生&#xff0c;你怎么看&#xff1f;二、对于即将实习/已经实习的学生&#xff0c;你有什么建议&#xff1f;1.学习方面2.提升方面三、思想成年真的很重要前言 最近&#xff0c;有一些同学遇到的实习问题&#xff0c;我统一回复下&…

第十二届蓝桥杯省赛详解

试题A&#xff1a;空间 1B是8位&#xff0c;32位二进制数占用4B空间&#xff0c;1MB2^10KB2^20B 那么可以存放32位二进制数的个数为256*2^20*8/3267108864 试题B&#xff1a;卡片 分析&#xff1a;因为数据只有2021&#xff0c;所以直接模拟即可 结果为&#xff1a;3181&…

MySQL基础------sql指令1.0(查询操作->select)

目录 前言&#xff1a; 单表查询 1.查询当前所在数据库 2.查询整个表数据 3.查询某字段 4.条件查询 5.单行处理函数&#xff08;聚合函数&#xff09; 6.查询时给字段取别名 7.模糊查询 8.查询结果去除重复项 9.排序&#xff08;升序和降序&#xff09; 10. 分组查询 1…

Linux 如何使用 git | 新建仓库 | git 三板斧

文章目录 专栏导读 一、如何安装 git 二、注册码云账号 三、新建仓库 配置仓库信息 四、克隆远端仓库到本地 五、git 三板斧 1. 三板斧第一招&#xff1a;git add 2. 三板斧第二招&#xff1a;git commit 解决首次 git commit 失败的问题 配置机器信息 3. 三…

最新!Windows 11 更新将整合 AI 技术

微软MVP实验室研究员张雅琪&#xff08;阿法兔&#xff09;微软最有价值专家&#xff08;MVP&#xff09;&#xff0c;毕业于外交学院和香港大学&#xff0c;IT 技术社区创始人&#xff0c;中关村互联网金融研究院兼职研究员&#xff0c;多次受邀在微软 Reactor 进行公开演讲&a…

电子工程师必须掌握的硬件测试仪器,你确定你都掌握了?

目录示波器示例1&#xff1a;测量示波器自带的标准方波信号输出表笔认识屏幕刻度认识波形上下/左右移动上下/左右刻度参数调整通道1的功能界面捕获信号设置Menu菜单触发方式触发电平Cursor按钮捕捉波形HLEP按钮参考资料频谱分析仪器信号发生器示波器 示例1&#xff1a;测量示波…

STM32F103R8T6 SPWM实现正弦波输出

前言 PWM合成正弦波&#xff0c;原理什么的不详细说了&#xff0c;概括一下就是 PWM有效面积的积分 正弦波的有效面积。PWM的频率越快&#xff0c;细分的越多&#xff0c;锯齿也就越不明显。 做法是&#xff1a;首先利用正弦波取点软件&#xff0c;取点1000个&#xff0c;生…

求职(怎么才算精通JAVA开发)

在找工作的的时候,有时候我们需要对自己的技术水平做一个评估。特别是Java工程师,我们该怎么去表达自己的能力和正确认识自己所处的技术水平呢。技术一般的人,一般都不敢说自己精通JAVA,因为你说了精通JAVA几乎就给了面试官一个可以随便往死里问的理由了。很多不自信的一般…

《ChatGPT是怎样炼成的》

ChatGPT 在全世界范围内风靡一时&#xff0c;我现在每天都会使用 ChatGPT 帮我回答几个问题&#xff0c;甚至有的时候在一天内我和它对话的时间比和正常人类对话还要多&#xff0c;因为它确实“法力无边&#xff0c;功能强大”。 ChatGPT 可以帮助我解读程序&#xff0c;做翻译…

在 4G 内存的机器上,申请 8G 内存会怎么样?

在 4GB 物理内存的机器上&#xff0c;申请 8G 内存会怎么样&#xff1f; 这个问题在没有前置条件下&#xff0c;就说出答案就是耍流氓。这个问题要考虑三个前置条件&#xff1a; 操作系统是 32 位的&#xff0c;还是 64 位的&#xff1f;申请完 8G 内存后会不会被使用&#x…

cmd命令教程

小提示&#xff1a; 在本文中&#xff0c;我将向您展示可以在 Windows 命令行上使用的 40 个命令 温馨提示&#xff1a;在本教程中学习使用适用于 Windows 10 和 CMD 网络命令的最常见基本 CMD 命令及其语法和示例 文章目录为什么命令提示符有用一、cmd是什么&#xff1f;如何在…

一年经验年初被裁面试1月有余无果,还遭前阿里面试官狂问八股,人麻了

最近接到一粉丝投稿&#xff1a;年初被裁员&#xff0c;在家躺平了6个月&#xff0c;然后想着学习下再去面试&#xff0c;现在面试了1个月有余&#xff0c;无果&#xff0c;天天打游戏到半夜&#xff0c;根本无法静下心来学习。下面是他这些天面试经常会被问到的一些问题&#…

手机解锁方法:8个顶级的 Android 手机解锁软件

一般来说&#xff0c;太简单的密码是不安全的&#xff0c;所以我们设置一个安全的密码&#xff0c;可能会稍微复杂一点。然而&#xff0c;我们可能经常会忘记复杂的密码并锁定我们的 Android 智能手机。 8个顶级的 Android 手机解锁软件 如果您遇到过这种情况并且正在寻找一种…

【Android -- 软技能】聊聊程序员的软技能

什么是软技能&#xff1f; 所谓软技能&#xff0c;就是相对于「硬技能」而言的技能&#xff0c;对于程序员来说&#xff0c;「硬技能」就是计算机专业技术能力&#xff0c;软技能则是专业之外的所有技能&#xff0c;包括职业规划能力、处理人际关系能力、专业态度、做事的方式…
最新文章