经过一段时间的学习,你已经是一名合格的C语言新手了,开始尝试更加high-level的知识吧!
本篇紧随上篇,主要讲:
1、语句的使用
2、函数部分的补充
3、指针
4、数组
一、语句的使用
1、表达式语句
表达式则表示值或计算过程,如`2 + 2`,可赋值给变量或用于计算。语句侧重于执行动作,表达式侧重于计算值。表达式可含运算符、变量等,而语句由主语和谓语构成。示例中,`x = 10`和`print("Hello, World!")`是语句,`y = x + 5`和`result = a * b + c`是表达式。
2、控制语句
控制语句为c语言较重要的一个部分
1、单分支语句:if
if会让程序执行以下的处理:
判断表达式的值,如果结果不为0,则执行相应的语句
2、双分支语句:if else
这里else是“否则”的意思
当表达式的值不为0的时候执行if对应语句,为0的时候执行else对应语句
3、多分支语句:
if elseif else
:语法:
if(条件){
//执行语句
}else if(条件){
//执行语句
}else{
//执行语句
}
switch语句:
switch(条件){
case 1:
//执行语句
break;
case 2:
//执行语句
break;
case 3:
//执行语句
break;
default:
//执行语句
}
猜数字小游戏
在这里,我们可以学习写一个猜数字小游戏,这包含几个部分:
1、生成一个随机数
我们可以用随机数函数srand,可srand生成的数字也并不完全随机
到时间戳,因为每时每刻时间都在改变所以可以确保其随机性
srand((unsigned int)time(NULL));
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
int i, n;
time_t t;
n = 5;
srand((unsigned)time(&t));
for (i = 0; i < n; i++) {
printf("%d\n", rand() % 50);
}
return(0);
}
2、判断所猜的数与所生成的数
if (answer < ret)
{
printf("小了,请重新输入\n");
printf("你还剩%d次机会。\n", i);
}
else if (answer > ret)
{
printf("大了,请重新输入\n");
printf("你还剩%d次机会。\n", i);
}
else if (answer == ret)
{
printf("恭喜你,猜对了,答案是%d\n", ret);
break;
}
if (i == 0){
printf("你输了。\n", i);
}
}
return 0;
}
3、酌情制作一个小菜单
void menu()
{
printf("###############################\n");
printf("######## 1.play ##########\n");
printf("######## 0.exit #########\n");
printf("###############################\n");
}
4、主体函数:只需要将写出来的部分组装出来即可
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <time.h>
#include <stdlib.h>
int i = 0;
void game()
{
int answer = 0;
srand((unsigned int)time(NULL));
int ret = rand() % 100 + 1;
while (1)
{
for (i = 4; i >= 0;i--) {
scanf("%d", &answer);
if (answer < ret)
{
printf("小了,请重新输入\n");
printf("你还剩%d次机会。\n", i);
}
else if (answer > ret)
{
printf("大了,请重新输入\n");
printf("你还剩%d次机会。\n", i);
}
else if (answer == ret)
{
printf("恭喜你,猜对了,答案是%d\n", ret);
break;
}
if (i == 0){
printf("你输了。\n", i);
}
}
return 0;
}
}
void menu()
{
printf("###############################\n");
printf("######## 1.play ##########\n");
printf("######## 0.exit #########\n");
printf("###############################\n");
}
int main()
{
int ui;
srand((unsigned int)time(NULL));
int ret = rand()%100+1;
do {
menu();
scanf("%d", &ui);
switch (ui)
{
case 1:
printf("已生成一个数字,请猜测:");
game();
break;
case 0:
printf("退出游戏");
break;
default:
printf("选择错误,请重新选择\n");
break;
}
}
while (ui);
return 0;
1、while循环
while (循环条件){ 执行语句};
2、for循环
for (循环变量赋初值;循环条件;循环变量增量){ 执行语句; }
3、do while 语句
do{ 执行语句; }while (循环条件);
转向语句:
continue:continue语句用于循环结构中,作用是跳过当次循环(并非全部循环),当循环语句执行到continue时,不会继续向下执行,会跳过当次循环
break:中断语句,一般用于循环结构中,作用是终止循环,当执行到break语句时,会立即退出循环
return:跳出当前一个函数语句,用于跳出函数并返回一个值。
goto:强制转向语句(不推荐使用)
二、函数的补充
上一篇中,我们已经普遍片面见识了一些函数,以下是进阶学习
1、库函数:c语言自带来实现一些常用功能的函数,是实现功能的基石
下面推荐一个查询库函数常用的网址:https://cplusplus.com/
2、自定义函数
程序员发挥的大舞台
由函数名、返回类型、参数、函数主体 组成
函数名:不用过多解释,关系到引用函数,大多数函数都可以用对应英文名的关键字命名方便记忆和后期引用如:get-prime(获取素数),max(求最大值函数)
返回类型:自定义函数可分为需要返回值与不需要返回值两种类型
需要返回值则得规定返回值的数据类型int float等等
参数:C语言函数的参数会出现在两个地方,分别是函数定义处和函数调用处,这两个地方的参数是有区别的。
形参(形式参数)
在函数定义中出现的参数可以看做是一个占位符,它没有数据,只能等到函数被调用时接收传递进来的数据,所以称为形式参数,简称形参。
实参(实际参数)
函数被调用时给出的参数包含了实实在在的数据,会被函数内部的代码使用,所以称为实际参数,简称实参。
形参和实参的功能是传递数据,发生函数调用时,实参的值会传递给形参。
attention:
交换两个变量的值:
需要借助一个中间变量,且依靠函数交换变量值的时候
函数参数的地址并不会因为函数的赋值而改变为变量的值导致无法交换,这时候我们需要依靠指针变量来为变量赋值
void swap(int x, int y)
{
int z = 0;//依靠中间变量来交换二值
z = x;
x = y;
y = z;
}
此时改变的只是形参的量,最后传出的结果无法影响到实参的值
void swap(int* pa, int* pb)//通过地址准确交换两个变量的值
{
int z = *pa;
*pa = *pb;
*pb = z;
}
运用指针,我们可以将值直接赋予实参所在的地址
函数主体:函数实现功能的关键。在此之前,我决定先讲讲函数的调用和声明
函数不能嵌套定义 , 但是可以嵌套调用
函数的调用:
1、传值调用:
把参数的实际值复制给函数的形式参数。在这种情况下,修改函数内的形式参数不会影响实际参数。
默认情况下,C 语言使用传值调用方法来传递参数。一般来说,这意味着函数内的代码不会改变用于调用函数的实际参数。
2、传址调用:多数情况下,我们通过对该地址值进行 引用操作,获取这个地址里的内容,所以,我们调用参数为指针的函数时,传递给函数的也是这个指针地址的拷贝,也就是所谓的“传递地址调用”
链式访问:
把一个函数的返回值当作另一个函数的参数
printf(“%d\n”,strlen(“abc”));
printf("%d", printf("%d", printf("%d",43)));
以上第二个函数打印的是:4321
why?答:printf函数返回的值是当前打印的字符数,43对应的字符数是2,2对应的字符数是1
故是4321
函数的声明:我们可能会遇到以下两种情况:1.函数主体位于引用该函数的后方,因为编译器是从上至下进行翻译,导致引用的函数无效
2.函数位于其他源文件,无法使用
对于第一种,我们可以先对函数进行一个声明,告诉计算机这是啥(先上车再补票)
int Add(int x, int y);
对于第二种,我们可以在头文件中对函数进行声明,让编译器对其进行一个预处理
include/
: 存放公共头文件src/
: 存放源文件lib/
: 存放库文件
在使用#include
指令时,可以使用尖括号<>
或双引号""
来指定头文件路径。尖括号用于系统头文件,而双引号用于用户头文件
函数递归:
这是对于新手来说特别难啃的一块
所谓递归,可以分为两个过程:函数递推以及函数回归
我们以一个递归函数举例:
(这个函数可以做到将一个整数依序打印它的每一位eg:1234----1 2 3 4 )
void print(unsigned int n)
{
if (n > 9)
{
print(n / 10);
}
pirntf("%d ", n % 10);
}
若我们输入123进入循环
首先123>9,进入第一个if语句,并以123/10=12的值进入下一个print自定义函数
因为12>9所以再以12/10=1的值进入一个print函数,像这样一个函数接着一个函数执行的叫作函数递推
(注:此时前两个的printf函数部分还未执行)
打印1_
再以12的值回到第二个print函数执行剩余部分,打印2_
以123的值回到第一个print函数,打印3_
像这样由最后一个函数向前返回执行剩余部分的,就是函数回归
利用函数递归,我们可以很轻松的做到很多通过循环很复杂的事(就是有点费脑子doge)
如:计算一个数的阶乘:
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int factorial(int n)
{
int sum=1;
for (int i = 1;i <= n;i++)
{
sum *= i;
}return sum;
}
int main()
{
int n;
scanf("%d", &n);
printf("%d", factorial(n));
return 0;
}
这是以循环方式做到的阶乘(更加稳定,不容易发生栈溢出)
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
int factorial(int n)
{
int sum=1;
if (n < 1)
{
return 1;
}
else {
return n *= factorial(n - 1);
}
}
int main()
{
int n;
scanf("%d", &n);
printf("%d", factorial(n));
return 0;
}
这就是以递推方式做到的阶乘,与高中数学中数列的递推公式大相径庭(其实直接搬通项公式加上c语言的if,else语句就可以做到一个递推)
如:斐波那契数数列
在数学中的通项公式
F(0)=0,F(1)=1, F(n)=F(n - 1)+F(n - 2)(n ≥ 2,n ∈ N*)。
而用递归方式写出来:
long Fib(int n)
{
if (n <= 2)
return 1;
else
return Fib(n-1) + Fib(n-2); //前两项加起来等于后一项
}
是不是很类似
三、数组
1、一维数组
数组是一组相同类型元素的集合。 数组的创建方式:
type_t arr_name [const_n];
//type_t 是指数组的元素类型
//arr_name 是数组的名字
//const_n 是一个常量表达式,用来指定数组的大小
数组的初始化:
完全初始化:数列的【】中相当于作为,数据相当于人,将所有座位坐满即为完全初始化
不完全初始化:当作为未坐满时,剩余的座位上就是0
- 数组是具有相同类型的集合,数组的大小(即所占字节数)由元素个数乘以单个元素的大小。
- 数组只能够整体初始化,不能被整体赋值。只能使用循环从第一个逐个遍历赋值。
一维数组:相当于一条直线,所有数据都位于同一条直线
这时要求数列的长度,建议使用
sizeof(arr)/sizeof(arr【0】)的形式来计算
一维数组的元素在地址上是连续的
eg:
#include <stdio.h>
int main()
{
int i = 0;
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int sz = sizeof(arr) / sizeof(arr[0]);
//打印数组的元素地址
for (i = 0; i < sz; i++)
{
printf("%p ", &arr[i]);
}
return 0;
运行结果:
因为这里是16进制传递的(123456789ABCDEF)可以看出每个数字占4个字节,且地址是线性连续传递的
2、二维数组
在一定场合,我们会有座位如第二行第二列,在数组中,我们同样存在不同于一条直线的一维数组的二维数组
int arr[3][4];//[行数][列数]
注:二维数组可以没有行数,但必须有列数
eg:
int arr[][5];
那二维数组元素之间地址是怎样排布的呢?
#include <stdio.h>
int main()
{
int arr[3][4];
int i = 0;
for (i = 0; i < 3; i++)
{
int j = 0;
for (j = 0; j < 4; j++)
{
printf("&arr[%d][%d] = %p\n", i, j, &arr[i][j]);
}
}
return 0;
}
运行结果:
由此可见:其实二维数组在内存中也是连续存储的
二维数组在内存的空间布局上,也是线性连续且递增的二维数组本质上也是一维数组,只不过内部元素放的是一维数组。
3、数组作为函数参数
数组元素就是变量,与普通变量没有区别,将数组元素传送给形参,实现单向的值传递
eg:
#include <stdio.h>
float max(float x,float y)
{
if(x > y)
return x;
else
return y;
}
int main()
{
int a[6] = {3,2,1,4,9,0};
int m = a[0];
for(int i = 1;i < 6; i ++)
{
m = max(m,a[i]);
}
printf("数组中的最大元素是:%d",m);
}
一个经典例子:冒泡数列
通过多次两个数组元素的比较来实现以大小关系对数组元素进行的排序
-
通过
sizeof
运算符计算数组的大小,这是一种动态获取数组长度的方法。 -
因为在bubble函数内部计算数组的大小会出现形参大小无法影响实参大小的情况,所以我们需要在函数外部就计算出函数的大小并传入函数
void bubble(int arr[],int sz) { for (int i = 0;i < sz-1;i++) { int j = 0; for (j = 0;j < sz - 1 - i;j++) { if (arr[j] < arr[j + 1]) { int tmp = arr[j]; arr[j] = arr[j+1]; arr[j+1]=tmp; } } } } #include<stdio.h> int main() { int sz; int arr1[10] = { 1,2,3,4,5,6,7,8,9,10 }; sz = sizeof(arr1) / sizeof(arr1[0]); bubble(arr1,sz); for (int q = 0;q < sz;q++) { printf("%d\n", arr1[q]); } return 0; }
4、数组与指针
与下一章一起介绍,在这就不介绍了
四、指针
不知道有多少小伙伴跟我一样,刚学指针时觉得指针完全没有存在的必要,用指针调用的东西,通过变量名,数组名,函数名都可以直接做到,但是指针的优点如是我们不可否认
因为指针直接接触的是内存中其对应的地址,使用指针可以提高性能访问性能
使用指针还可以优化内存使用和避免内存泄漏,除此之外,指针还有许多好处,同学们,学起来!!!
1、指针的定义
指针也就是内存地址,指针变量是用来存放内存地址的变量。就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其进行声明。指针变量声明的一般形式为: type *_name;
type 是指针的基类型,如int,float,char
_name 是指针变量的名称。
数据类型 | 数据类型 | 指向该数据类型的指针 | 指向该数据类型的指针 |
---|---|---|---|
(unsigned) char | 1字节 | (unsigned) char* | x字节 |
(unsigned) short | 2字节 | (unsigned) short* | x字节 |
(unsigned) int | 4字节 | (unsigned) int* | x字节 |
(unsigned) long | 4字节 | (unsigned) long* | x字节 |
float | 4字节 | float* | x字节 |
double | 8字节 | double* | x字节 |
2、指针的应用
1.传递参数
使用指针传递大容量的参数,主函数和子函数使用的是同一套数据,避免了参数传递过程中的数据复制,提高了运行效率,减少了内存占用
使用指针传递输出参数,利用主函数和子函数使用同一套数据的特性,实现数据的返回,可实现多返回值函数的设计
2.传递返回值
将模块内的公有部分返回,让主函数持有模块的“句柄”,便于程序对指定对象的操作
3.直接访问物理地址下的数据
访问硬件指定内存下的数据,如设备ID号等
将复杂格式的数据转换为字节,方便通信与存储
4.在变量声明的时候,如果没有确切的地址可以赋值,为指针变量赋一个 NULL
值是一个良好的编程习惯。
赋为 NULL
值的指针被称为空指针
。NULL
指针是一个定义在标准库中的值为零的常量
eg:
#include <stdio.h>
int main() {
int* p = NULL;
printf("p 的地址是 %p\n", p);
return 0;
}
3、指针的算数运算
-
指针加法:当你对指针进行加法运算时,指针会增加一个或多个它所指向的数据类型的大小。例如,如果你有一个指向
int
的指针,在32位系统中,每个int
占用4个字节,那么ptr + 1
将指向下一个int
的地址。 -
指针减法:与加法相反,减法会使指针向前移动,即减少它所指向的数据类型的大小。
-
指针比较:你可以比较两个指针是否相等或不等,或者比较它们的大小。比较通常用于判断两个指针是否指向数组的相同位置,或者一个指针是否在另一个指针之前或之后。
-
指针增量:你可以递增指针(
ptr++
),这会使指针移动到下一个元素的地址。 -
指针减量:你可以递减指针(
ptr--
),这会使指针移动到上一个元素的地址。
#include <stdio.h>
int main() {
int arr[5] = { 10, 20, 30, 40, 50 };
int* ptr = arr;
// 指针加法
ptr = ptr + 2; // ptr现在指向arr[2],即30
printf("%d\n", *ptr);
// 指针减法
ptr = ptr - 1; // ptr现在指向arr[1],即20
printf("%d\n", *ptr);
// 指针增量
ptr++; // ptr现在指向arr[2],即30
printf("%d\n", *ptr); // 输出30
// 指针减量
ptr--; // ptr现在指向arr[1],即20
printf("%d\n", *ptr);
return 0;
}
4、指针与数组
-
数组名作为指针:数组名可以作为指向数组第一个元素的指针。例如,如果有一个数组
int arr[10];
,那么arr
和&arr[0]
是等价的,都指向数组的第一个元素。 -
数组的地址:可以通过
&arrayName[index]
获取数组元素的地址。 -
指针访问数组元素:指针可以用于遍历数组。例如,
int *ptr = arr;
后,可以通过*(ptr + index)
来访问数组的元素。
eg:
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;
// 使用指针访问数组元素
for (int i = 0; i < 5; i++) {
printf("%d\n", *(ptr + i)); // 等同于printf("%d\n", arr[i]);
}
return 0;
}
如图*(ptr+i)就是用指针来访问的数组中的元素
4.数组作为参数:在函数中,数组作为参数传递时,实际上是传递了数组首元素的地址。
5.指针运算:指针可以进行加法和减法运算,例如ptr + index
将指向数组的第index
个元素。
6.指针与数组大小:指针的大小取决于它指向的数据类型。例如,指向int
的指针在32位系统中占用4字节,在64位系统中占用8字节,而与数组的大小无关。
7.多维数组与指针:多维数组可以通过指针和指针的指针来处理。例如,二维数组可以看作是指针的数组,每个元素本身也是一个指针。
#include <stdio.h>
int main() {
int arr[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
// 使用指针访问
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("Element at [%d][%d]: %d\n", i, j, *(*(arr + i) + j));
}
}
return 0;
}
输出:
5、指针与函数
1。函数指针是指向函数的指针变量。
通常我们说的指针变量是指向一个整型、字符型或数组等变量,而函数指针是指向函数。
函数指针可以像一般函数一样,用于调用函数、传递参数。
函数指针变量的声明:
int (*fun_ptr)(int,int);
声明一个指向同样参数、返回值的函数指针类型
2.指针作为函数参数
通过指针,函数可以修改传递给它的数据。
#include <stdio.h>
void increment(int *num) {
(*num)++;
}
int main() {
int value = 10;
increment(&value);
printf("%d\n", value); // 输出11
return 0;
}
补充:野指针以及如何避免野指针
1. 指针未初始化:指针变量刚被创建时不会自动成为NULL指针,它所指的空间是随机的。
int main()
{
int * p;
*p = 6;
return 0;
}
2. 指针越界访问:指针指向的范围超出了合理范围,或者调用函数时返回指向栈内存的指针或引用
3 .指针释放后未置空:当一个指针指向的内存被释放后,如果没有将该指针设置为NULL
(在C++中是nullptr
),那么该指针仍然保留其原始的地址值,但这块内存已经不再被当前程序所拥有。此时,如果程序继续使用这个指针,就会产生未定义的行为,可能导致程序崩溃或数据损坏。
#include <stdio.h>
#include <stdlib.h>
int main() {
int* ptr = (int*)malloc(sizeof(int)); // 动态分配内存
if (ptr == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return 1;
}
*ptr = 42;
printf("Value: %d\n", *ptr); // 输出值
free(ptr); // 释放内存
ptr = NULL; // 置空指针
return 0;
}
本人这部分学得较为簸,之后会再写一篇进行补充对学习成果的检验:C语言刷题
如果喜爱本篇blog的话,还望各位点赞加收藏,这都是对作者最大的鼓励