C语言语法
第一个C程序
1 |
|
本课程中所有的程序都需要用到这一段框架(直到学函数之前)
printf()会把" "内的内容(字符串)原封不动地输出,\n表示换行。
如果出现编译错误,会在下面用红色的error显示出来,而具体错误原因在下方窗口里。
简单计算
1 | printf("%d",23+43); |
四则运算 C符号 意义
-
- 加
-
- 减
× * 乘
÷ / 除
% 取余
( ) ( ) 括号
- 减
变量:
变量定义
找零钱问题:100-(用户输入)物品的价格=找你的钱
我们需要:
有办法输入数字;
有地方放输入的数字;
输入的数字参与计算。
1 | int price=0;//定义了整形变量price,类型是int,初始值=0 |
变量是一个保存数据的地方。
变量定义的一般形式就是:
<类型名称> <变量名称>
变量的名字叫**“标识符”,**基本原则只能由字母、数字、下划线构成,数字不能出现在第一个位置上。C语言的关键字(又叫保留字)不可用作字符。
输入:
输入也在终端窗口中。输入以行为单位进行,敲下回车,行结束。
(敲下回车之前,程序不会读到任何东西。)
变量赋值与初始化
1 | int price=0;//其中=是赋值运算符,把右边的值赋给左边的变量。 |
也可以在组合变量定义中给单个变量单独赋初值,如
int a=0,b=1;
有运算符的式子就叫表达式(比如=是赋值运算符)
C语言是有类型的语言,所有变量在使用前必须先定义或声明;所有变量必须有确定的数据类型(表示在变量中可以存放什么样的数据),变量中也只能存放指定类型的数据,程序运行过程中也不能改变变量的类型。
printf("请输入金额(元):");
scanf("%d",&price);
change=100-price;
printf("找您%d元。\n",change);
变量输入:
scanf()函数,读到的结果赋值给后面的变量
(注意变量前的&)
如果输入非整数(比如找钱一例中:输入字母,找我100元)应该怎么处理?这种情况后面再说。
常量vs变量
固定不变的数,是常数。直接写在程序里的,叫直接量。
const int amount=100;
1
其中const是个修饰符,加在int前面,给这个变量加上一个const(不变的)属性,表示这个变量的值一旦初始化,就不能再更改了。
好处:1.便于理解2.修改的时候好找
编译时会显示:read-only variable is not assignable
(一般const的变量全大写)
scanf的空格有讲究 输入时如果要两个数,在中间、最后敲空格或回车,计算机读到两个数字时停止
当scanf扫描数字时输入字母,可能会出来很多奇怪的数字。(应该是ASCII码吧?)后面再细说。
浮点数
C语言中两个整数做运算得到的结果也只能是个整数(去掉小数部分)。而10和10.0在c中是完全不同的两个数字,10.0是浮点数(浮点数指小数点时可以浮动的,是计算机中表示分数和无理数的一种方式。人们用浮点数来称呼有小数点的数)(其实还有定点数。但是在c语言中无)
当浮点数和整数放在一起运算时,计算机会自动把整数转化为浮点数计算
(如:
1 |
|
有一次尝试printf(“%f”,3/2);输出的是整数。应该是先进行整数运算再以%f形式输出,而不是以%f形式输出。所以应该改进为3/2.0
单精度float(%f)和双精度double(%lf)
(计算机里会有这种纯粹的整数,是因为运算快、占地小。而且日常生活中大多也是整数运算。)
表达式
一个表达式是一系列运算符和算子的结合,用来计算一个值
运算符(operator)是指进行运算的动作,比如加/减法运算符±
算子(operand)是指参与运算的值,可能是常数/变量/一个方法的返回值
计算时间差:这里只记录分钟错位的情况(1:40和2:10)
方法:全部记为分钟(小时*60)
1 | int hour1,min1; |
字符:
在C语言中,字符是一种基本的数据类型,用来存储单个字符。字符数据类型使用 char
关键字表示,每个字符变量占用一个字节的内存空间。字符在C中以ASCII码形式表示,例如,字符 ‘A’ 对应的ASCII码是65。
以下是一个简单的字符示例:
1 |
|
在这个例子中,我们定义了一个字符变量 myChar
,并使用 %c
和 %d
格式说明符分别输出字符和它的ASCII码值。
C语言提供了一些用于字符处理的标准库函数,例如 isalpha
(判断字符是否是字母)、isdigit
(判断字符是否是数字)、toupper
(将小写字母转换为大写)等。下面是一个使用这些函数的简单示例:
1 |
|
在这个例子中,我们使用 isalpha
和 isdigit
函数判断字符的类型,并使用 toupper
和 tolower
函数将字符转换为大写或小写形式。
字符串:
在C语言中,字符串是字符数组,以空字符 '\0'
结尾。C语言没有内置的字符串类型,但通过使用字符数组和一些字符串处理库函数,可以方便地处理字符串。
以下是一个简单的字符串示例:
1 |
|
在这个例子中:
char greeting[6] = {'H', 'e', 'l', 'l', 'o', '\0'};
定义了一个字符数组greeting
,存储了字符串 “Hello”。注意,数组的大小是 6,以便存储字符串和结尾的空字符'\0'
。%s
是printf
和scanf
等函数中用于处理字符串的格式说明符。strcat
函数用于将两个字符串连接起来。
注意,C语言中的字符串处理需要小心防止缓冲区溢出等问题。确保字符数组足够大,以容纳字符串及其结尾的空字符。在现代C中,也可以使用字符串字面值来初始化字符数组,例如 char greeting[] = "Hello";
这样的语法更简洁。
运算符优先级
1 | (double输入的时候用%lf,输出的时候用%f就行) |
result=a=b=3+c;
result=(result=result*2)*6*(result=3+result);
断点:
在编译器中,在左边数字那里点击一下,然后程序运行时就会在这一行停止。
运行也只能到这一行
点下一行,就会运行到下一行。
这样可以方便排bug
C语言:使用Dev C++断点调试
复合赋值
a+=5表示a=a+5(注意两个运算符之间无空格)
类似有-=,=,/=
(a=b+5表示a=a*(b+5),如果想先算a=a*b记得加括号)
INC递增、DEC递减运算符:++,–:单目运算符,只能有一个算子,这个算子还只能是变量(如a++后缀形式,++a前缀形式)让该变量+1/-1;
a++运算式的值是a+1以前的值;++a运算式的值是a+1以后的值。无论哪个,a的值都+1了
++和–可以单独使用,但不要把它组合进表达式,复杂。
判断(选择结构)
if条件判断
1 | if(条件){ |
计算两个值之间的关系,叫做关系运算
==判断相等
!=不相等
还有>;>=;<;<=,当两个值的关系复合这些关系运算符的预期时,关系运算的结果为整数1,否则为整数0
1 | printf("%d\n",5>3); |
嵌套的if-else
1 | #找三个数中的最大数:先比较两个数谁更大,较大的再和c比较。 |
级联的if-else if
如分段函数。这时可以用级联的else if 表达多种情况
每个else是前一个if的else,但是前面两个都不满足的话就只剩下第三个了
1 | else |
更建议上面的方法,有单一出口(都是输出f),因为代码要降低重复性,出问题的时候才好统一处理。
if-else的常见错误:
忘记写大括号的话,条件成立时只执行后面一句
If后面无分号
==和=:if只要求括号里是0或非0
使人困惑的else
尊重warning!
大括号内的语句加tab缩进是代码风格。
多路分支:
1 | switch(print){ |
与if的自顶向下判断不同,switch从几个case中找出3后执行,就是说几个case的判断是同时的.
但是switch语句只能判断int整形变量,而且case后面的必须是常量(如mon=1,case 1)
Switch是先跳到对应的case处,然后一条条向下执行直到遇到break跳出switch
如果进入下一个case前还没有碰到break,那就进入下一个case
按成绩分等级的题:可以先/10,再分case。
注意一定要在每个case后加上break
1 | int i=1; |
解释:switch是一个路标牌,计算控制表达式的值后,就知道跳转到哪了(在哪里上车),但是分支符号只是说明switch内部位置的路标,在执行完分支后,如果后面没有break(下车提示),就会顺序执行到下面的case,直到遇到break,或者switch结束(终点站)
也就是说,程序跳到了one那个case,然后因为没有break就继续往下顺延,又输出了two。
循环:
while 循环:
1 |
|
do while
1 | do |
请注意,条件表达式出现在循环的尾部,所以循环中的 statement(s) 会在条件被测试之前至少执行一次。
如果条件为真,控制流会跳转回上面的 do,然后重新执行循环中的 statement(s)。这个过程会不断重复,直到给定条件变为假为止
for循环:
1 | for ( init; condition; increment ) |
循环的嵌套:
循环的嵌套是指在一个循环体内包含另一个循环。这种结构在处理多维数据结构(如二维数组)或者需要对一组元素进行复杂操作时非常常见。已经提到的二维数组的遍历就是一个很好的例子。
下面是一个更具体的例子,演示了两个嵌套的 for
循环用于输出乘法表:
1 |
|
在这个例子中,外层循环控制行(i
变量),而内层循环控制列(j
变量)。通过嵌套循环,可以逐行逐列地输出乘法表的结果。
请注意,循环的嵌套不仅仅局限于 for
循环,while
循环同样可以嵌套。选择使用哪种循环结构取决于问题的性质和个人偏好。
数据类型
C语言是有类型的语言。C语言的变量,必须:
1.在使用前定义;
2.确定类型.
整数:char、short、int、long、long long;
浮点数:float,double,long double;
逻辑:bool;
指针;
以及自定义类型。
类型的不同之处在于:
类型名称:int,long,double……
输入输出时的格式化(占位符):%d,%ld,%lf……
所表达的数的范围:char<short<int<float<double
内存中占据的大小:1~16个字节
内存中的表达形式:二进制数(补码)、编码(浮点是编码形式)
编码形式不能直接进行运算,而两个整数可以。
Sizeof()是一个运算符,给出某个类型或变量在内存中所占据的字节数。如:
1 | sizeof(char)=1;//char1字节(8比特) |
整数的内部表达:
计算机内部,一切都是二进制的,只是说我们以不同的方式去看待它。
十进制:18→二进制:00100010
十进制用负号来表示负数,运算一般在结果上再加负号。
而二进制怎么表示负数呢?
一个字节(8位)可以表达的数:00000000 ~ 11111111(0~255)
三种方案:
像十进制一样,有一种特殊的标志(类似符号)来表示负数(缺陷:计算机做加减法的时候,要像判断十进制的负号一样,我们需要一个东西去控制加号还是减号。不过每次计算机都要特别地去判断这个符号的正负,这样就会比较复杂。)
从0000 0000到1111 11111,取中间的数为0,如1000 0000表示0,比他小的是负数,比他大的是正数,各一半(缺陷:所有数都要和这个数做减法来计算其值,会比较复杂)
补码
思路:本质上来看,(互为相反数的)负数+正数=0。这是提供思路的一种方法。
比如我们希望-1+1→0,如何能够做到?
如0→0000 0000,1→0000 0001,我们让一个数+1得到0。这个数字选谁?
全1的数字1111 1111。因为0000 0001+1111 1111→1 0000 0000多出来的一位(溢出)那一位被丢掉了,相加结果即是00000000。妙啊
或者换个角度:-1=0-1=(1)0000 0000-0000 0001→1111 1111
(1111 1111被当作纯二进制看待时是255,被当做补码看待时是-1)
所以对于-a来说,他的补码就是0-a,实际就是2n-a,n是该种类型的位数
补码的意义就是拿补码和原码可以加出一个溢出的0。
另一个好处是这样我们做计算的时候,不需要调整加减,全部都是加法(+补码就是-原码)。
整数的范围:如何推算整数类型所能表达的数的范围,越界了会怎样?
一个字节(8位):0000 0000~1111 1111
其中0000 0000→0
0000 0001 ~ 0111 1111→1~127(纯二进制数)
1000 0000 ~ 1111 1111→-128 ~ -1(用补码表示的数)
还是那句话,关键在于我们以什么样的方式去看待这个数。当成纯二进制数,1111 1111就是255;当成整数,就是-1
1 | char c=255; |
1 | 类型 大小 范围 |
unsigned类型:
如果我们希望一个数据从计算机中拿出来时我们将其视为纯二进制看待,我们要在其前面加上一个关键字unsigned.
unsigned char c=255;
unsigned使得这个类型在正整数表达部分范围扩大一倍,但是不能表达负数。
如果一个字面量常数想要表达自己是unsigned,可以在后面加u/U:255U
想表达long,后面加l
unsigned的初衷不是为了扩展数能表达的范围,而是为了做纯二进制运算,主要是为了移位。
整数是以纯二进制方式进行运算的。
整数的格式化:
如处理8进制/16进制
整数的输入输出只有两种形式:int和long long
1 | %d:int |
若int i=-1;储存在计算机中是二进制形式,即全1,若以%u即unsigned形式输出,得到的结果就是int 型的最大值(4294967295)
8进制(octal):一个以0开始的数字字面量
16进制(Hexadecimal):一个以0x开始的数字字面量
1 | char c=012; |
(计算机内部同样还是二进制的形式)
想要输出8进制:%o或%O
想输出16进制:%x或%X,小写的x输出小写的字母,大写的输出大写的
printf(“c=0%o,i=0x%x\n”,c,i);
8进制和16进制只是如何把数字表达为字符串,与内部如何表达数字无关。
scanf中也可以用%o,表示读进来的数我们把其当做8进制来读。
16进制很适合表达二进制数据,因为4位二进制正好是一个16进制位(0001 0010→1 2)
八进制的一个数字正好表达3位二进制,因为早期计算机的字长是12的倍数,并非8。
空格的问题:
1 | scanf("%d%c",&i,&c); |
这样中间几个空格回车都没关系,中间的空格会全部被读掉
从A到Z的ASCII码都是连着的
一个字符加一个数字可以转到后面那个对应ASCII码的字符处
int i=‘Z’-‘A’;//两个字符相减,得到它们在表中的距离
字母在ASCII表中顺序排列,且大小写字母分开排列
‘a’-‘A’可以得到小写字母与大写字母之间的距离,所以’a’+(‘A’-‘a’)可以转小写为大写
逃逸字符:
用来表达无法印出来的控制字符或特殊字符,由一个反斜杠\开头,后面跟着一个字符
printf(“请分别输入身高的英尺和英寸,”“如输入"5 7"表示5英尺7英寸:”);
1 | 1 |
类型转换
自动类型转换:
当运算符两边出现不一样的类型时,会自动转成较大的类型,即能表达的数范围更大的类型。
char->short->int->long->long long
int->float->double
对于printf,任何小于int的类型都会被转换成int;float会被转换成double。所以printf输出double的时候写%f就也行。
但scanf不会,想输入short时,需要%hd。
强制类型转换:(类型)值:
1 | (int)10.2; |
是注意安全性,小的变量不总能表达大的量。如(short)32768,因为short最大范围是32767,所以会被转化为-32768
只是从那个变量计算出了一个新类型的值,他并不改变那个变量的值或类型。
i输出仍然是32768。强制类型转换不会改变这个变量自身的类型或值。
如果想计算a,b的int型相除得到的i的int型的值:
1 | double a=1.0; |
实际上是先int a,再/浮点数b,强制类型转换的优先级高于四则运算。
所以正确的写法是
1 | int i=(int)(a/b); |
逻辑类型:
表示关系运算和逻辑运算结果的量
bool类型
首先要包含头文件#include<stdbool.h>,然后可以使用bool,true,false。
bool b=6>5;
只要bool量不是0,都=1.
逻辑运算:对逻辑量进行与、或、非运算
逻辑运算是对逻辑量进行的运算,结果只有0或1。
逻辑量是关系运算或逻辑运算的结果。
运算符 描述 示例 结果
! 逻辑非 !a a的true或false反转
&& 逻辑与 a&&b 只有a&b都是true时结果才是true
|| 逻辑或 a||b 只有a&b都是false时结果才是false
所有的优先级:
优先级 运算符 结合性
1 () 从左到右
2 !,+,-,++,– 从右到左(单目的+和-)
3 *,/,% 从左到右 4 +,- 从左到右 5 <,<=,>,>= 从左到右 6 ==,!= 从左到右 7 && 从左到右 8 || 从左到右 9 =,+=,-=,*=,/=,%= 从右到左
短路:逻辑运算自左向右,如果左边足以决定结果,就不会进行右边的计算了
条件运算符:
条件运算符1:?
1 | cnt=(cnt>20)?cnt-10:cnt+10; |
这种条件运算符?的优先级高于赋值运算符,但是低于其他运算符,
函数:
函数的定义和调用:
函数是一块代码,接收0个/多个参数做一件事情,并返回0个/1个值。
调用函数时要写出 函数名(参数值);
()起到了表示函数调用的重要作用,即使没有参数我们也需要()。
不给括号的话,会给warning,而且函数不会被调用。
参数的数量、顺序都必须正确。而且函数知道每一次是哪里调用它,还会返回到正确的地方。
从函数中返回:
int函数会有返回值return
return停止函数的执行,并返回一个值
return;
return 一个值;
可以写 c=函数();这样c=函数的返回值。
可以把这个值赋给变量/传给函数/丢掉。
没有返回值的函数:void,不能使用带值的return ,可以没有return 。调用的时候也不可以做返回值的赋值。
函数原型:用来告诉编译器这个函数长什么样
使用函数的先后顺序:先写函数再调用。
因此也可以先不用写完函数,光把一句函数头放到前面编译也能通过。
1 | /*(事先声明了函数的样子) |
函数头以分号结尾,就构成了函数的原型;
在函数里定义的参数类型与输入的变量的类型不一样,会发生自动类型转换。
double max(double a,double b);
在以前是把函数原型写在调用它的函数(main())里面的。
函数的原型里可以不写参数名,但一般仍然写上。
Void sum(int ,int)
例:
1 | void swap(int a,int b)//形参 |
C语言在调用函数时,永远只能传值给函数。
本地变量:
定义在函数内部的变量是本地变量,参数也是本地变量
函数每次运行都会产生一个独立的变量空间,其中的变量是函数这一次运行所独有的。(本地变量)
所有我们定义在函数内部的变量就是本地变量。(我们现在学过的都是定义在函数内部的)我们写在函数参数表里的参数也是本地变量。
变量的生存期和作用域
生存期:这个变量出现和消亡的时间
作用域:在(代码的)什么范围内可以访问这个变量(这个变量可以起作用)
对于本地变量,这两个答案都是:大括号内(块)
1 | void swap(int a,int b)//形参 |
仍然是上例,进入swap函数之后(离开了自己的变量空间)a,b就没了。还在生存,但是不在当前的作用域了(显示:Not found in current context)
而回到原函数中之后,x,y,t就不存在了
所以在swap函数里交换a,b不会影响到原函数
本地变量的规则
本地变量定义在块内。可以是函数的块内,语句的块内,如:
if (a<b)int i=10;
离开if语句之后未定义使用i编译错误。(没声明)
程序运行进入这个块前,其中的变量不存在。离开后就消失了。
如果在块里定义了块外已经定义过的变量,就把块外的变量掩盖了;出来之后又回到块外的值。(C语言)
但是不能在块里面定义同名变量(多次定义)
本地变量不会被默认初始化,不会得到一个初始值。而参数进入函数的时候被初始化了。
函数庶事:一些细节,main()的解释
函数没有参数的时候写void f(void)。而写void f()在传统C语言中表示f的函数的参数表未知,并不表示没有参数。(编译器可能会猜测什么类型)
所以不要写空的括号
调用函数时的逗号和逗号运算符怎么区分?再加一层括号就是逗号运算符f((a,b))
C语言不允许函数嵌套定义。
(也最好不要写return (i);虽然意思上没变,但是会让人误会return 是个函数)
int main(void)也是个函数,所以return 0;也是有意义的。
数组:
int number[100];//定义数组,表示数组可以放100个int
scanf(“%d”,&x);
while(x!=-1){
number[cnt]=x;//对数组中的元素赋值
cnt++;
scanf(“%d”,&x);
}
最后再加个cnt长度的循环,判断每一个数与平均数比较大小
if(cnt>0){
int i;
double average=sum/cnt;
for(i=0;i<cnt;i++){
if(number[i]>average){//使用数组中的元素
printf("%d ",number[i]);//遍历数组
}
}
}
这个程序的安全隐患在于没有考虑使用的数组下标是否会超过100.定义的时候注意要求。
数组的使用:
如何定义和使用数组,数组的下标和下标的范围
定义数组:
<类型>变量名称[元素数量];//方括号表示这是个数组
int grades[100];
double weight[20];
元素数量必须是整数。在c99之前,元素数量必须是编译时确定的字面量。(a[n]不行)vscode中好像就不行,提示variable-sized object may not be initialized
数组是一种容器,特点是:
其中所有元素具有相同的数据类型;
一旦创建,不能改变大小;
其中元素在内存中连续依次排列(从0开始);
如:定义十个单元a[10]→a[0]~a[9]
每个单元就是一个int类型的变量。像普通变量一样可以出现在赋值的左边或右边。左边的叫左值,右边的叫右值。
数组的每个单元就是数组类型的一个变量。使用数组时[]中的数字/变量叫下标或索引,从0开始计数
(要习惯数数从0开始到n-1)
但编译器和运行环境不会检查数组下标是否越界,无论读、写数组单元。
不过数组越界时可能出问题:segmentation fault,运气好的话不会造成严重的后果。
所以这是程序员的责任来保证程序只适用有效的下标值(范围:[0,数组大小-1])
防止读入数字超过100个的方法:
方法一:cnt=100之后停止读数;
方法二:利用c99数组大小可以是动态的的特性,定义number[cnt];//用户先输入cnt
可不可以int a[0];?
可以,但是没用。
数组的例子:统计个数
不停输入0~9范围内的整数,读到-1停止,统计每种数字出现的次数。
和上一道题不同的是,不用记录每次输入的数字,我们需要记录的是每种数字出现的次数。
学到了定义数组为0的方法:
for(int i=0;i<10;i++)count[i]=0;
`
和打印方法:
for(int i=0;i<10;i++)printf(“%d\n”,count[i]);
该题中出现多次数字10。根据之前学到的方法,我们可以定义const number=10(c99才能用);每一个10用number代替。
通常用到数组的程序都需要的环节:
确定数组大小;
定义数组;
初始化数组;
数组参与运算;
遍历数组输出。
数组运算
搜索:在一组给定数据中,怎样找出某个数据是否存在?
(往函数中传数组:int sum(a[]))
数组的集成初始化:
int a[]={2,4,6,7,1,3,5,9}
/直接用大括号给出数组所有元素的初始值;
不需要给出数组的大小,编译器替你数了。/
依次初始化数组的每一个单元:
如果a[13]={2};
只有a[0]是2,后面的单元都是0
所以如果想定义一个数组全为0:a[13]={0};
C99还可以在大括号里给指定的位置赋值。
用[n]在初始化数据中给出定位,没有定位的数据接在前面的位置后面;其他位置的值补0.
int a[0]={[0]=2,[2]=3,6};
这个例子里,a[0]=2,a[2]=3,a[3]=6
我们也可以不给出数组大小,让编译器计算。比如上例可写为:
int a[]={[0]=2,[2]=3,6};
这样会根据大括号里最大的数字下标来算数组的大小。即下标最大为3
这样特别适合初始数据稀疏的数组。
数组的大小:
sizeof给出整个数组所占据的内容的大小,单位是字节。(n*4,sizeof(a)/sizeof(a[0])就能得到数组元素个数)
不能直接把一个数组赋给另一个数组b[]=a;
遍历数组:
数组变量本身不能被赋值。如果想把一个数组的值全部交给另一个数组,必须遍历。
通常使用for循环,从0开始到<n,这样循环体最大的i正好是数组最大的有效下标。
常见错误:1.循环结束条件是<=数组长度
1 |
|
1 |
|
二维数组:
在C语言中,二维数组是一个数组的数组,通常用于表示矩阵或表格。以下是一些关于如何遍历二维数组的示例:
1 |
|
指针:
指针是一种在程序中存储变量地址的变量类型。通过使用指针,可以间接访问内存中的数据。在C语言中,指针是一项强大而灵活的特性,可以用于处理数组、动态内存分配以及函数参数等。
以下是一些关于指针的基本概念和用法:
-
指针的声明和初始化:
-
nt *ptr;
: 这一行声明了一个整型指针ptr
。在C语言中,指针的类型应该与它指向的变量的类型相匹配。这里int *
表示ptr
是一个指向整数的指针。 -
ptr = &x;
: 这一行将指针ptr
的值设置为变量x
的地址。&x
表示取变量x
的地址。因此,ptr
现在包含了变量x
的内存地址。 -
这行代码使用
printf
函数输出变量x
的地址。具体解释如下:1
printf("Address of x: %p\n", &x);
-
"Address of x: %p\n"
: 这是一个格式化字符串,其中%p
是一个格式说明符,用于输出地址。在运行时,%p
会被实际的地址值替代。 -
&x
: 这是取变量x
的地址的操作符。&
运算符返回变量在内存中的地址。
当这一行代码执行时,它会打印出变量
x
的内存地址。例如,输出可能类似于:1
Address of x: 0x7ffdbec3a12c
这个地址的具体值会根据你的计算机和操作系统的不同而变化。
%p
输出的是十六进制形式的地址值。在这行代码中,你使用了
printf
函数来输出指针ptr
的值,即它所指向的地址。以下是详细解释:1
printf("Value stored in ptr: %p\n", ptr);
-
"Value stored in ptr: %p\n"
: 这是一个格式化字符串,其中%p
是格式说明符,用于输出地址。在运行时,%p
会被实际的地址值替代。 -
ptr
: 这是整型指针,存储了一个地址值。
当这一行代码执行时,它会打印出指针
ptr
中存储的地址值。输出结果可能类似于:1
Value stored in ptr: 0x7ffdbec3a12c
这个地址的具体值会根据你的计算机和操作系统的不同而变化。
%p
输出的是十六进制形式的地址值。这个地址值与前面输出的变量x
的地址应该是相同的,因为ptr
被赋值为变量x
的地址。 -
-
1 | int main() { |
- 指针和数组:
1 | int main() { |
- 指针和动态内存分配:
1 | int main() { |
在这个例子中,malloc
函数用于动态分配内存,而 free
函数用于释放动态分配的内存。
指针是一个广泛用于C语言中的概念,它提供了对内存的直接访问,但也需要小心使用,以避免潜在的错误和问题,如空指针引用和内存泄漏。
指针的内存地址和指针所包含的变量的内存地址是两个不同的概念。让我们详细解释它们之间的区别:
- 指针的内存地址:
- 指针本身也占用内存,并且它有自己的地址。这个地址是指针变量在内存中存储的位置。
- 通过使用取地址运算符
&
可以获取指针本身的地址。例如:&ptr
将返回指针变量ptr
的地址。
- 指针包含的变量的内存地址:
- 指针变量的主要作用是存储另一个变量的内存地址。这个变量可以是任何数据类型,例如整数、字符、数组等。
- 通过解引用运算符
*
可以访问指针所指向的变量的值,而这个变量的内存地址可以通过直接使用指针变量得到。
以下是一个简单的例子,说明了这两个概念的区别:
1 |
|
在这个例子中:
&ptr
获取指针变量ptr
的地址。ptr
存储了变量x
的地址。*ptr
通过解引用获取了指针ptr
所指向的变量x
的值。
请注意,在使用 printf
输出指针地址时,使用 %p
格式说明符。 (void*)
是一种类型转换,用于确保正确的输出。
结构体:
结构体(Struct)是C语言中一种用于存储不同数据类型的集合的复合数据类型。结构体允许你将多个相关的变量组织在一起,形成一个单一的数据单元。每个变量称为结构体的成员(member)或字段(field)。
以下是一个简单的结构体的例子:
1 |
|
在这个例子中,我们定义了一个名为 Person
的结构体,它有三个成员:name
(字符数组)、age
(整数)和 height
(浮点数)。然后,我们创建了一个结构体变量 person1
,并通过结构体成员访问和修改变量的值。
结构体在C语言中非常有用,特别是在处理复杂的数据结构时。你可以通过嵌套结构体、数组和指针等方式,创建更为复杂的数据结构。
您提到的结构头指针和函数指针可能涉及到不同的概念,我将简要介绍一下它们。
-
结构头指针(Struct Head Pointer):
在C语言中,结构体是一种用户自定义的数据类型,用于存储不同类型的数据。每个结构体实例都包含多个成员(member),而结构头指针则指向结构体的第一个成员的地址。结构头指针通常用于访问结构体的各个成员。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15struct Point {
int x;
int y;
};
int main() {
struct Point p;
struct Point *ptr = &p;
// 使用结构头指针访问结构体成员
ptr->x = 10;
ptr->y = 20;
return 0;
}在上面的例子中,
ptr
是指向struct Point
结构体的头指针,通过ptr->x
和ptr->y
可以分别访问结构体的成员。 -
函数指针:
函数指针是指向函数的指针变量。在C语言中,函数指针可以用来传递函数作为参数,实现回调函数等功能。声明函数指针时,需要指定函数的返回类型和参数类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
int (*operation)(int, int); // 声明一个函数指针
operation = add; // 函数指针指向 add 函数
printf("Sum: %d\n", operation(5, 3));
operation = subtract; // 函数指针指向 subtract 函数
printf("Difference: %d\n", operation(5, 3));
return 0;
}在上面的例子中,
operation
是一个函数指针,它可以指向add
函数或subtract
函数,从而调用不同的函数。
如果您有特定的问题或需要更详细的解释,请提供更多上下文,我将尽力提供帮助。
宏定义:
宏定义是一种在C语言中用于创建简单替代文本的机制。使用宏定义,你可以给一段代码或一个表达式起一个名称,并在程序中多次使用这个名称,而不必每次都重复写相同的代码。
宏定义使用 #define
关键字,通常在程序的开头或头文件中定义。以下是一个简单的宏定义的示例:
1 |
|
在这个例子中,#define PI 3.14159
将文本替换为 PI
,并将 PI
定义为一个常量,表示圆周率。之后,代码中的 PI
将被替换为 3.14159
。
除了简单的常量替代,宏定义还可以带有参数,类似于函数。以下是一个带参数的宏定义的示例:
1 |
|
在这个例子中,#define SQUARE(x) ((x) * (x))
定义了一个带参数的宏,用于计算一个数的平方。在使用 SQUARE
宏时,x
将被替换为传递给宏的实际参数。在上述例子中,SQUARE(num)
将被替换为 (num) * (num)
,最终计算出 5 * 5
的结果。
在C语言中,typedef
是一种用来创建新的数据类型名称(类型别名)的关键字。typedef
语句可以用于为现有的数据类型创建一个新的、更容易理解的名称。这对于简化代码并提高可读性非常有用。
typedef:
以下是一个简单的 typedef
的示例:
1 |
|
在这个例子中,typedef int INTEGER;
将 int
类型定义为一个新的数据类型名称 INTEGER
。这样,在程序的其他部分就可以使用 INTEGER
来声明整数类型的变量。
typedef
通常用于创建更具有描述性的类型别名,以提高代码的可读性。例如,你可能会看到一些库使用 typedef
来创建自定义的数据类型,使得代码更易读并且更具表达力。以下是一个更复杂的示例:
1 |
|
在这个例子中,typedef struct {...} Point;
创建了一个新的数据类型名称 Point
,该类型是一个包含 int
类型成员 x
和 y
的结构体。
文件:
在C语言中,文件的输入与输出主要通过标准库中的文件操作函数来实现。以下是一些基本的文件输入与输出的操作:
文件的打开与关闭
-
打开文件:
1
2
3
4
5
6FILE *fptr; // 文件指针
fptr = fopen("filename.txt", "r"); // 打开文件以供读取("w" 表示写入,"a" 表示追加,"r+" 表示读写等)
if (fptr == NULL) {
printf("Error opening file.\n");
return 1;
} -
关闭文件:
1
fclose(fptr);
读取文件
-
逐字符读取文件:
1
2
3
4char ch;
while ((ch = fgetc(fptr)) != EOF) {
printf("%c", ch);
} -
逐行读取文件:
1
2
3
4char line[100];
while (fgets(line, sizeof(line), fptr) != NULL) {
printf("%s", line);
}
写入文件
-
逐字符写入文件:
1
fputc('A', fptr);
-
逐行写入文件:
1
fprintf(fptr, "Hello, World!\n");
格式化输出
- 格式化输出到文件:
1
fprintf(fptr, "Integer: %d, Float: %.2f, String: %s\n", 10, 3.14, "Hello");
这只是文件输入与输出的基本示例。注意,在使用文件操作函数时,要确保文件是否成功打开,以及在使用完文件后关闭文件,以防止资源泄漏。错误处理也是一个重要的方面,以确保程序在面对文件操作错误时能够适当地处理。
递归函数:
递归(Recursion)是一种编程技术,其中函数可以调用自身。C语言也支持递归,允许一个函数在其定义中调用自己。递归函数通常在解决问题的过程中能够简化代码逻辑。
以下是一个简单的例子,展示了一个递归函数在C语言中的用法。这个例子使用递归计算阶乘(factorial):
1 |
|
在这个例子中,factorial
函数是一个递归函数,它通过不断调用自身来计算阶乘。递归的关键是定义递归的基本情况(base case),以及在递归情况下如何将问题规模减小。在这里,基本情况是当n
等于0或1时,阶乘为1。递归情况则是利用递归关系式n! = n * (n-1)!
。
需要注意的是,递归虽然能够简化代码,但如果不谨慎使用,可能导致栈溢出(stack overflow)或者性能问题。因此,在使用递归时,确保定义了合适的基本情况,避免无限递归。
链表:
链表是一种常见的数据结构,用于存储一系列元素,其中每个元素都包含一个值和指向下一个元素的指针。链表和数组不同,它不需要一块连续的内存空间,而是通过节点(node)之间的指针连接起来。
在C语言中,链表通常由节点结构体和指向节点的指针组成。以下是一个简单的单链表(单向链表)的例子:
1 |
|
这个例子演示了如何定义链表节点结构体(struct Node
),以及如何使用函数来创建节点并将其添加到链表末尾。在 printList
函数中,我们使用循环遍历链表并打印节点的值。
需要注意的是,为了避免内存泄漏,需要在不再使用链表时释放节点所占用的内存。在实际应用中,还可以实现其他链表操作,如删除节点、插入节点等。