C语言 (笔记)
C语言
C是编译型语言,先编译才运行,解释型直接运行
1.初识C
#include<stdio.h> //引用头文件
int main() //main() 代表主函数,有且仅有一个
{
printf("Hello World\n"); //{}:大括号里的是函数体,每行都有缩进(四个空格或者tab)和在结尾加 ; 表示结束
return 0; //printf默认不换行(python默认换)需要加转义字符 \n 换行
}
注释:单行 // ,多行 /**/
2.变量
例:
#include<stdio.h>
int main()
{
// 变量的声明
int a; // 可以声明一个或多个变量,但是类型要相同
int b, c;
// 变量的初始化
a = 1; // 上一步可以直接加上变量,就可以省略这一步了 like:int a = 1
b = 2,c = 3;
// 变量的使用
printf("a=%d,b=%d\n", a , b); // %d是占位符
// 变量的命名
int first = 1; // 变量名只能用数字、字母、_命名,而且不能使用数字开头,不能用key word
return 0;
}
3.输入
scanf输入,需要变量接收,变量前要有 &(指针)
只能输入不能输出(还是python简单直接input完事)
4.定义常量
有两种方式可以定义常量
1.#difine 常量名 常量
用difine定义常量,常量名需要大写(不成文的规定,比较好认)
difine在编译前就改变常量
2.在函数体内 const 数据类型 常量名 = 常量
const在运行时如果检测到常量改变就会报错
const补充(指针)
const 修饰变量,这个变量被称为常变量,不能被修改,本质还是变量
const修饰指针变量,如果放在*的右边,修饰的是指针变量,表示指针变量不能修改,但是指针指向的内容可以被改变
int n = 1;
int * const p = &n;
*p = 2;
如果const放在*的左边,修饰的是指针指向的内容,是不能通过指针来改变的,但是指针变量本身可以被修改
int n = 1,m = 2;
int const * p = &n;
p = &m;
5.数据类型
5.1 整数类型
不同的系统,字节长度可能是不一样的
共有四种数据类型:short, int, long, long long
数据类型 | signed格式化 | unsigned格式化 |
---|---|---|
short | %hd | %hu |
int | %d | %u |
long | %ld | %lu |
long long | %lld | %llu |
5.2 浮点类型
float,double, long double
默认小数直接量是double类型
数据类型 | 输出格式化 | 输入格式化 |
---|---|---|
float | %f | %f |
double | %f | %lf |
long double | %Lf | %Lf |
如果只想保留n位小数就只需要在%后面加 .n
示例 | |
---|---|
保留2位小数 | %.2f |
保留5位小数 | %.5f |
float至少精确至6位小数,double至少精确至10位
浮点不能穷尽数字,只能返回近似值
5.3 字符类型
字符类型(char)不是字符串类型(string)
字符用 '' 单引号,字符串用 "" 双引号
数据类型 | 字节 | 格式化 |
---|---|---|
char | 1 | %c |
5.4 布尔类型
布尔类型只有true(1)或false(0)
布尔类型有两种表示:
数据类型 | 返回结果 |
---|---|
_Bool | 1 和 0 |
bool | ture 和 false |
bool : 要引入<stdbool.h>才能使用
可以用 %d 接受布尔类型结果的返回值
5.5 类型转换
在=号的后面加上(type),就可以强制转换类型
union
union是一种数据类型,它能在同一个内存空间中存储不同的数据类型(不是同时存储) |
---|
union的使用方式与结构体相同,需要先定义union类型,再声明并初始化union类型的变量 |
编译器会分配足够的内存空间,以便于该空间能够储存union声明中占用最大字节的类型 |
int main()
{
union score {char level; double value;};
union score stu_1 = {'A'};
union score stu_2 = {.value = 99.99};
stu_1.value = 99.99;
stu_2.level = 'A';
return 0;
}
枚举
有时候一组数据是有限且固定的,比如性别、季节、方向等 |
---|
可以通过定义整形类型的常量或枚举类型来表示这样的数据 |
enum season {spring, summer, autumn, winter}; |
通过enum定义了season类型,它内部包含了4个枚举常量 |
实际上枚举常量是int类型,默认为从0开始的连续的值 |
可以在需要整数的地方(for、switch)使用枚举类型 |
#include <stdio.h>
int main()
{
enum season {spring, summer, autumn, winter};
for (int i = spring; i <= winter; ++i) {
printf("%d\n", i);
}
return 0;
}
/*output:
0
1
2
3
*/
6. 运算符
常用 | 运算符 |
---|---|
算术运算符 | + - * / % |
递增与递减运算符 | ++ -- |
关系运算符 | > < >= <= == != |
逻辑运算符 | && || ! |
位运算符 | & | ~ ^ << >> |
赋值运算符 | = += -= *= /= %= &= |= ^= <<= >>= |
三元运算符 | ?: |
递增与递减运算符
++ 或 -- 就是把操作数递增或递减1
前缀模式( ++i ):先让操作数递增/减,在让其参与运算
后缀模式( i++ ):先参与运算,再让操作数递增/减
三元运算符
条件 ? 结果1 : 结果2
如果条件为真就输出结果1否则就结果2
#include <stdio.h>
int main()
{
int age;
printf("请输入年龄!\n");
scanf("%d", &age);
printf("%s", age >= 18 ? "已成年" : "未成年");
return 0;
}
// 输入18,结果返回值“已成年”
7.流程控制
if语句(分支语句)
if (控制条件) // 如果只有一行代码可以不用 {} 但是不建议
{代码}
else if (控制条件)
{代码}
else if (控制条件)
{代码}
else
{代码}
// 请输入成绩,90分以上为优,75为良,60分及格,60分以下为不及格
#include <stdio.h>
int main()
{
int score ;
printf("请输入你的成绩!\n");
scanf("%d", &score);
if (score >= 90)
{
printf("成绩为优!\n");
}
else if (score >= 75)
{
printf("成绩为良!\n");
}
else if (score >= 60)
{
printf("成绩合格!\n");
}
else
{
printf("不合格!\n");
}
return 0;
}
switch语句
switch (表达式)
{
case 常量值1:
...
break;
case 常量值2:
...
break;
case 常量值3:
...
break;
default:
}
注意点:
1.表达式和常量值必须返回整数类型
2.case可以有多条语句
3.default语句可以省略
// 输入天数返回星期几
#include <stdio.h>
int main()
{
int day;
printf("请输入第几天\n");
scanf("%d", &day);
switch(day)
{
case 1:
printf("现在是星期一\n");
break;
case 2:
printf("现在是星期二\n");
break;
case 3:
printf("现在是星期三\n");
break;
case 4:
printf("现在是星期四\n");
break;
case 5:
printf("现在是星期五\n");
break;
case 6:
printf("现在是星期六\n");
break;
case 7:
printf("现在是星期七");
break;
default:
printf("别搞");
return 0;
}
}
4.如果case没有break语句,程序会继续输出下面case的值,直到遇见break
// 输入月份返回季节
#include <stdio.h>
int main()
{
int month;
printf("请输入月份!\n");
scanf("%d", &month);
switch (month) {
case 3:
case 4:
case 5:
printf("现在是春季\n");
break;
case 6:
case 7:
case 8:
printf("现在是夏季\n");
break;
case 9:
case 10:
case 11:
printf("现在是秋季\n");
break;
case 12:
case 1:
case 2:
printf("现在是冬季\n");
break;
default:
printf("别搞\n");
}
return 0;
}
8. 循环语句
while循环
while(循环条件){
...
[迭代语句]
}
while : 先判断循环条件,再执行循环
迭代语句 : 一个变量每次循环变化,最后达到循环条件跳出
// 输入一个数字,返回是几位数
#include <stdio.h>
int main()
{
int num;
printf("请输入数字\n");
scanf("%d", &num);
int count = 0;
while(num)
{
num /= 10;
count++;
}
count = count == 0? 1 : count;
printf("是%d位数\n", count);
return 0;
}
do...while循环
do
{
...
[迭代语句]
}while(循环条件);
do...while特性 : 先执行一次,再判断循环条件
// 与上面的例子相比,最后不需要三元运算符比较再输出
#include <stdio.h>
int main(){
int num;
int count = 0;
printf("输入一个数字\n");
scanf("%d", &num);
do
{
num /= 10;
count++;
}while(num);
printf("是一个%d位数\n", count);
return 0;
}
for循环
// for (初始化语句;循环语句;迭代语句)
// {
// ...
// }
// 输入一个自然数,计算累加和
#include <stdio.h>
int main(){
long long num;
printf("输入一个自然数\n");
scanf("%lld", &num);
long sum = 0;
for (long round = 0; round <= num; round++)
{
sum += round;
}
printf("%ld\n", sum);
return 0;
}
三种循环的特点
for 循环:结构稳定,不容易遗漏任何循环要素,适合处理循环次数固定的场景
while 循环:先判断循环条件再执行循环体,适合“当...”的场景
do...while 循环:先执行一遍再判断循环条件,适合“直到...”的场景
break 语句
结束循环,强制跳出循环体
continue 语句
跳出此轮循环,执行下一轮循环
9. goto语句
goto 标签 ;
标签:...
// 计算三个1~100数的和等于222
#include <stdio.h>
int main()
{
int n , m , h;
for (n = 1; n < 101; n++)
{
for (m = 1; m < 101; m++)
{
for (h = 1; h < 101; h++)
{
if (n + m + h == 222)
{goto resulted;}
}
}
}
resulted:
printf("%d %d %d\n", n, m, h);
return 0;
}
一般应用场景:当需要一次跳出多层循环时
注意:goto语句尽量不用,容易乱套
猜字游戏
// 猜字游戏,猜对了才能结束
#include <stdio.h> // printf,scanf...x
#include <time.h> // 引入时间的头文件
#include <stdlib.h> // srand,rand 需要用到的头文件
int main()
{
srand(time(NULL)); // 修改种子,因为rand()是伪随机数
int target = rand(); // 定义target为随机数
target %= 100; // 让target处于100以内
printf("猜1到100的数字!\n");
int guess;
while(1) // while为真,死循环
{
scanf("%d", &guess);
if (guess == target)
{
printf("可以,猜对了!\n");
break;
}
else if (guess > target)
{
printf("大了大了,再小点!\n");
}
else
{
printf("小了点,搞大点!\n");
}
}
return 0;
}
breakpoint :断点
Debug : 调试
10. 函数
返回类型 函数名(参数) // 函数头
{...} // 函数体
#include <time.h> // 导入相关头文件
#include <stdlib.h>
#include <stdio.h>
// 随机数1~100
int run()
{
srand(time(NULL));
return rand() % 100 + 1;
}
// 输出hello world字符串
void hello()
{
printf("Hello World!\n");
}
注意点:
要是没有参数,参数可以为空
return代表结束并返回类型
如果没有返回类型,则需要声明为void
__attribute__((unused)) void GroupOrNot(int age) // 输入年龄,判断是否成年
{
if (age < 0) // 避免用户输入负数
{
return; // return直接结束,没有返回值
}
printf("%s", age > 18 ? "已成年" : "未成年");
}
函数原型
期望的代码结构
main函数在前面,自定义的函数在main函数后,使得程序结构清晰,便于理解和查看
函数原型的作用
在调用函数前,对函数做出明确的声明,使得函数在定义前就可以得到调用
函数原型的使用
返回类型 函数名(参数); // 在调用前声明,让编译器认识
extern int run(); // 函数头加个; 就是声明了
extern void hello(); // e
#include <time.h>
#include <stdlib.h>
#include <stdio.h>
int main(){
...
}
// 随机数1~100
__attribute__((unused)) int run()
{
srand(time(NULL));
return rand() % 100 + 1;
}
// 输出hello world字符串
__attribute__((unused)) void hello()
{
printf("Hello World!\n");
}
__attribute__((unused)) void hello(void)
{
printf("Hello World!\n");
}
// hello()的参数空着用户也能输入且不报错,如果不想让用户输入任何参数可以在()中输入void,确认不能输入参数
函数声明与定义补充
在工程中一般把函数的声明放到自创建的.h文件中,函数的定义放到另一个.c文件中,需要使用时 #include "自创建的.h文件"
#include "" 是调用在此工程中已修改的头文件
#include <> 是调用文件夹中的头文件
11. 递归
一个函数调用了它自己就是递归
递归是隐性循环
#include <stdio.h>
int fibonacci(int n); // 定义函数
int main()
{
printf("%d", fibonacci(20)); // 输出斐波那契数列前二十位
}
// 计算斐波那契数列
// 1, 1, 2, 3, 5, 8, 13, 21, 34, 55
// 斐波那契数列的第n位等于前面两位的和
// n = (n - 1) + (n - 2)
int fibonacci(int n)
{
if (n == 1 || n == 2)
{
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
12. 数组
概念:数组是非常常见的数据结构,可以存储多个类型相同的数据
特点:1.数组中的每个元素都具有相同的数据类型
2.数组元素的固定的,长度无法改变
3.数组中的元素按线性方式(有序)排列,可以通过索引查询每个数据
数组的声明:type arrayName[length];
数组的初始化:type arrayName[length] = {element1, element2, element3};
如果在声明时直接初始化可以不写长度,编译器会自动识别 type arrayName[];
数组的访问:arrayName[index];
index就是数组的索引(下标),下标是从0开始的
第一个元素是arrayName[0],最后一个是arrayName[length - 1]
#include "stdio.h"
int main()
{
char levels[4] = {'A', 'B', 'C', 'D'};
printf("%c %c %c %c\n", levels[0], levels[1], levels[2], levels[3]);
return 0;
}
// 结果是:A B C D
遍历数组
for (int i = 0; i < length; i++)
{
arrayName[i];
}
数组的length可以用数字,但是不建议
两种方法:
1.定义常量
# difine length number
const int length = number;
2.计算出长度
int length = sizeof(arrayName) / sizeof(arrayName[0]);
#include "stdio.h"
int main()
{
char levels[4] = {'A', 'B', 'C', 'D'};
// printf("%c %c %c %c\n", levels[0], levels[1], levels[2], levels[3]);
int length = sizeof(levels) / sizeof(levels[0]);
for (int i = 0; i < length; i++)
{
printf("%c ", levels[i]);
}
return 0;
}
内存地址
数组相邻的元素之间相差固定的N个字节,N是该数组元素的类型大小
访问地址:
转换运算符 | %p |
---|---|
寻址运算符 | & |
#include "stdio.h"
int main()
{
char levels[4] = {'A', 'B', 'C', 'D'};
int n[] = {1, 2, 3, 4};
int length_levels = sizeof(levels) / sizeof(levels[0]);
int length_n = sizeof(n) / sizeof(n[0]);
for (int i = 0; i < length_levels; i++)
{
printf("level[%d]的内存地址%p\n", i, &levels[i]);
}
for (int i = 0; i < length_n; i++)
{
printf("n[%d]的内存地址时%p\n", i, &n[i]);
}
return 0;
}
/* 我运算出的结果
level[0]的内存地址0xffffcc1c
level[1]的内存地址0xffffcc1d
level[2]的内存地址0xffffcc1e
level[3]的内存地址0xffffcc1f
n[0]的内存地址时0xffffcc00
n[1]的内存地址时0xffffcc04
n[2]的内存地址时0xffffcc08
n[3]的内存地址时0xffffcc0c*/
多维数组
// 多维数组(j)
#include "stdio.h"
int main()
{
int month[4][7] =
{
{1, 2, 3, 4, 5, 6, 7},
{1, 2, 3, 4, 5, 6, 7},
{1, 2, 3, 4, 5, 6, 7},
{1, 2, 3, 4, 5, 6, 7}
};
int size1 = sizeof(month) / sizeof(month[0]);
int size2 = sizeof(month[0]) / sizeof(month[0][0]);
for (int i = 0; i < size1; ++i)
{
for (int j = 0; j < size2; ++j)
{
printf("%d ", month[i][j]);
}
printf("\n");
}
// 内存地址
for (int i = 0; i < size1; ++i)
{
for (int j = 0; j < size2; ++j)
{
printf("%p ", &month[i][j]);
}
printf("\n");
}
return 0;
}
/* 结果
1 2 3 4 5 6 7
1 2 3 4 5 6 7
1 2 3 4 5 6 7
1 2 3 4 5 6 7
0xffffcba0 0xffffcba4 0xffffcba8 0xffffcbac 0xffffcbb0 0xffffcbb4 0xffffcbb8
0xffffcbbc 0xffffcbc0 0xffffcbc4 0xffffcbc8 0xffffcbcc 0xffffcbd0 0xffffcbd4
0xffffcbd8 0xffffcbdc 0xffffcbe0 0xffffcbe4 0xffffcbe8 0xffffcbec 0xffffcbf0
0xffffcbf4 0xffffcbf8 0xffffcbfc 0xffffcc00 0xffffcc04 0xffffcc08 0xffffcc0c
*/
保护数组数据
下面用求数组和的代码做例子
我们这个代码就是求数组和,传进来后不希望数组的变量在函数里被修改,我们就在函数形参加个const
这样在函数内就不数组就不能被修改了
// 计算数组的和
__attribute__((unused)) int sum(const int * arr, int size)
{
// int length;
// printf("输入数组长度!\n");
// scanf("%d", &length);
int s = 0;
for (int i = 0; i < size; ++i) {
s += *(arr + i);
}
return s;
}
13. 指针
指针的运算
运算 | 示例 | 说明 |
---|---|---|
取地址 | &i | 获取变量i的地址 |
解引用 | *p | 获取指针p所指向的地址上储存的值 |
赋值 | p = &i | 将变量i的地址存入指针p中 |
比较 | p1 < p2 | 判断指针p1是否小于p2,前提是二者指向相同类型的数据 |
与整数相加 | p + 3 | 在数组中,将指针p向右移动3个元素的位置 |
与整数相减 | p - 3 | 在数组中,将指针p向左移动3个元素的位置 |
递增 | p++,++p | 在数组中,将指针p向右移动1个元素的位置 |
递减 | p--,--p | 在数组中,将指针p向左移动1个元素的位置 |
求差 | p2 - p1 | 在数组中,计算指针p1、p2所指元素之间相隔几个元素 |
指针本质上是一个内存地址为变量的值
地址运算符(&)
已知变量result,&result就是获取result的地址
间接运算符(*)or 解引用运算符
已知指针变量p,则*p是获取指针p所指向的地址上的值
声明指针变量时必须指定指针所指向的变量的类型
例:int * parr
使用指针一般在指针名前加p
*和指针名之间的空格可有可无,一般声明时用上,解引用时省略
打印指针时转换说明符 %p |
---|
函数参数小结
函数称谓
定义函数时声明的参数叫形参,调用函数时传入的实际的值叫实参
两种形式
1.定义形参为普通的类型,调用时传入与改类型匹配的值,如 fun(x);
2.定义形参为指针的类型,调用时传入一个合法的地址值,如 fun(&x);
选择依据
如果 要计算或处理值,则才用第1种形式的函数调用。如要在函数中改变主调函数的变量,则采用第2种形式的函数调用。scanf()函数就是第2种形式的调用。
变量类型小结
编写程序时,可以认为变量有两个属性:名称、值
加载程序后,可以认为变量有两个属性:地址、值,地址就是变量在计算机内部的名称
普通变量
普通变量把值作为基本量,把地址作为通过&运算符获得的派生量
指针变量
指针把地址作为基本量,把值作为通过*运算符获得的派生量
数组指针的使用
例:arr[] = {...} arr == %arr[0]
指针加1指的就是参加1个储存单元,对数组而言,加1后的地址就是下一个元素的地址
*arr+1 == &arr[1] , (arr+1) == arr[1]
c语言标准在描述数组表示法时借助了指针!
- *定义arr[i] 的意思就是 (arr + i)
- 先在内存中找到arr位置,然后移动 i 个单元,检索存储在那里的值
- *不要混淆*(arr + i)和 arr + i,间接运算符( * )的优先级高于算术运算符 ( + )
#include "stdio.h"
int main()
{
short codes[] = {1, 2, 3, 4, 5};
short * pcodes = codes;
int length = sizeof(codes) / sizeof(codes[0]);
for(int i = 0; i < length; i++)
{
printf("%p\n", pcodes);
printf("%d\n", *(pcodes));
}
printf("\n \n \n");
char scores[] = {'A', 'B', 'C', 'D', 'E'};
char * pscores = scores;
int size = sizeof(scores) / sizeof(scores[0]);
for(int i = 0; i < size; i++)
{
printf("%p\n", pscores+i);
printf("%c\n", *(pscores+i));
}
return 0;
}
sizeof(数组名) - 数组名表示整个数组 - 计算的是整个数组的大小,单位是字节
&数组名 - 数组名表示整个数组 - 取出的是整个数组的地址
定义函数
int sum(int * arr);
int sum(int arr[]);
只有在函数定义的头部或者函数原型中才能用“int arr[]”代替“int * arr”
#include "stdio.h"
__attribute__((unused)) int sum(int * arr, int size);
int main()
{
int nums[] = {1, 2, 3, 4, 5};
int size = sizeof(nums) / sizeof(nums[0]);
printf("%d\n", sum(nums, size));
return 0;
}
// 计算数组的和
__attribute__((unused)) int sum(int * arr, int size) {
// int length;
// printf("输入数组长度!\n");
// scanf("%d", &length);
int s = 0;
for (int i = 0; i < size; ++i) {
s += *(arr + i);
}
return s;
}
14. assert(断言)
(1)使用断言捕捉不应该发生的非法情况。不要混淆非法情况与错误情况之间的区别,后者是必然存在的并且是一定要作出处理的。
(2)使用断言对函数的参数进行确认。
(3)在编写函数时,要进行反复的考查,并且自问:"我打算做哪些假定?"一旦确定了的假定,就要使用断言对假定进行检查。
(4)一般教科书都鼓励程序员们进行防错性的程序设计,但要记住这种编程风格会隐瞒错误。当进行防错性编程时,如果"不可能发生"的事情的确发生了,则要使用断言进行报警。
ASSERT ()是一个调试程序时经常使用的宏,在程序运行时它计算括号内的表达式,如果表达式为FALSE (0), 程序将报告错误,并终止执行。如果表达式不为0,则继续执行后面的语句。这个宏通常原来判断程序中是否出现了明显非法的数据,如果出现了终止程序以免导致严重后果,同时也便于查找错误。
ASSERT 只有在 Debug 版本中才有效,如果编译为 Release 版本则被忽略。
15. 大小端
大端(储存)模式,是指数据的低位保存在内存的高地址中,而数据的高位,保存在内存的低地址中
小端(储存)模式,是指数据的低位保存在内存的低地址中,而数据的高位,保存在内存的高地址中
#include <stdio.h>
int main()
{
int a = 1;
char * p = (char *)&a;
if (*p == 1)
{
printf("大端");
}
else
{
printf("小端");
}
return 0;
}
16. 动态内存分配
void * malloc(size_t * size)
向malloc申请的空间大小是以字节为单位的
返回的结果是void ,需要类型转换为自己需要的类型*
example: (int * )malloc(n * sizeof(int))
使用malloc需要引用头文件<stdlib.h>
malloc( n ) | 说明 |
---|---|
1.malloc() 会找到一个n字节的空闲的内存块 | |
2.malloc()不会为改内存块命名,它直接返回该内存块的首字节地址 | |
3.malloc()的返回类型是void指针 (void * ) 它相当于一个通用指针 | |
4.malloc()的返回值通常被强制转换为匹配的类型,如double * | |
5.malloc()存在分配内存失败的可能性,当失败时它会返回空指针(NULL) |
每次申请完空间后需要释放
free( p ) | 说明 |
---|---|
参数p是指针,指向之前malloc返回的地址 | |
free()不能用于释放通过其他方式分配的内存 |
17. 字符串
-
用双引号括起来的内容称为字符串字面量,编译器会自动在其末尾追加空字符 \0
-
字符串的结束标志是\0, 注: '\0' != '0', '\0' = 0, '0'在ASCII码表里是48
-
如果多个字面量之间没有间隔,或者只有空白字符分隔,C会将其视为串联起来的字面量 example:“Hello” “World”
-
字面量被视为指向该字符串储存位置的指针
char * string = "Hello World"; |
---|
string是一个指针,初始化为指向一个字符串常量 |
string指向的字符串不能被修改,实际上string是 const char * string |
如果需要修改字符串应该使用数组:char string[] = "Hello World" |
指针字符串 char * string 一般用于形参和动态分配空间
字符串的两种定义方式
数组表示法
char arr[3] [10] = {"123", "456", "789"}; |
---|
arr 是一个数组,该数组包含三个字符串,每个字符串的最大长度为10
指针表示法
char * parr [3] = {"123", "456", "789"}; |
---|
parr 是一个数组,该数组包含三个指针元素,每个元素都指向一个字符串
字符串的输入与输出
输入字符串
- 先分配储存空间(数组表示法),再读取输入的字符串
- C语言提供了多个输入字符串的函数,如 gets(), fgets(), scanf() ...
输出字符串
- C语言提供了多个输出字符串的函数,如 puts(), fputs(), printf() ...
使用gets()需要字符数组接收
gets(str); | 说明 |
---|---|
它读取整行输入,直到遇到换行符为止 | |
它会丢弃换行符,储存剩余字符,并在末尾追加一个空字符 | |
它无法检测输入了多少元素,不管有无超过数组限制元素 | |
在C11标准下gets()函数无法使用 |
puts(str) | 说明 |
---|---|
传入字符串地址即可,它从传入地址开始打印,直到遇到空字符 | |
puts() 函数在打印字符串时,它会自动在这个字符串的末尾增加一个换行符 |
#include <stdio.h>
int main()
{
// gets()
char name[10];
gets(name);
// 输入hello world!
puts(name);
printf("%s\n", name);
return 0;
// 返回两个hello world!
// 一个是puts函数,一个是printf函数
// 输入了12个字符,输出了13个字符
#include <stdio.h>
int main()
{
// puts()
puts("Hello World");
char str[] = "Hello World";
puts(str);
char * pst = "Hello World";
puts(pst);
puts(&str[6]);
puts(pst + 6);
return 0;
}
/* 返回
Hello World
Hello World
Hello World
World
World
*/
fgets(str, size, stdin) | 说明 |
---|---|
第二个参数声明了可以读取的最多字符数,参数为N则可读取数为N-1个字符 | |
在读取第N-1个字符前遇到换行符也会停止读取,并会将换行符也存储在字符串里 | |
第三个参数指明要读入的文件,如果要读取从键盘输入的数据则以stdin作为参数 | |
该函数返回char的指针,通常返回第一个参数的地址,当函数读取到了文件的末尾返回一个空指针(NULL) |
fputs(str, stdout) | 说明 |
---|---|
第二个参数指明要写入的文件,如果要打印在显示器则以stdout为参数 | |
函数不会在输出的末尾追加换行符 |
#include <stdio.h>
#define SIZE 10
int main()
{
char str[SIZE];
fgets(str, SIZE, stdin);
fputs(str, stdout);
return 0;
}
// input: 12345678910
// o123456789
scanf() | 说明 |
---|---|
用scanf输入字符串时,读取到空字符就会结束 | |
可以通过转换说明符指定宽度,"%10s" - 最多读取10个字符 | |
函数返回一个整数值,该值等于成功读取的项数或EOF(-1,读到文件结尾时返回EOF) |
printf() 打印多个数据(包括字符串)时更加方便 |
---|
字符串函数(常用)
strlen
strlen(char *) 括号里传入字符指针,计算字符串长度,strlen不会计算字符串末尾的'\0',而sizeof会 |
---|
#include <stdio.h>
#include <string.h>
int main()
{
char str[] = "<string.h>";
printf("%d", strlen(str));
return 0;
}
// output: 10
------------------------------------------------------------------------------------------------------------
#include <stdio.h>
#include <string.h>
int main()
{
char * str = "<string.h>";
printf("%d", strlen(str));
return 0;
}
// output: 10
strcat
strcat(str1, str2) | 说明 |
---|---|
将第二个字符串追加到第一个字符串的末尾 | |
第1个字符串会改变,第2个字符串还是原来的字符串 | |
该函数返回类型为"char *",实际返回第1个参数的地址 |
#include <stdio.h>
#include <string.h>
int main()
{
char str1[] = "Hello",str2[] = "World";
strcat(str1, str2);
puts(str1);
puts(str2);
return 0;
}
// output: HelloWorld
// World
/* 注意:如果是指针数组不能用此函数
char * str1 = "Hello",* str2 = "World";
strcat(str1, str2);
*/
strncat
strncat(str1, str2, n) | 说明 |
---|---|
strncat和strcat基本一样 | |
不同处 | 第3个参数指定了最多追加的字符数,函数在追加到n个字符或提前遇到空字符时停止 |
#include <stdio.h>
#include <string.h>
#define SIZE 10
int main()
{
char str1[SIZE] = "Hello", str2[] = "World";
strncat(str1, str2, SIZE - 1 - strlen(str1));
puts(str1);
return 0;
}
// output: HelloWorl
// SIZE - 1 - strlen(str1)算出还剩下多少空间存放字符,减1是预留一个空间给'\0'
strcpy
strcpy(char *, char *) | 说明 |
---|---|
将第2个字符串的内容拷贝到第1个字符串 | |
函数返回类型为“char *”,实际返回第1个参数的地址 | |
第1个参数可以不指定数组的开始 如:(str1 + 2),这样可以只覆盖数组的一部分 |
#include <stdio.h>
#include <string.h>
int main()
{
char str1[] = "Hey", str2[] = "World";
char * string = strcpy(str2, str1);
puts(str1);
puts(str2);
puts(string);
return 0;
}
// 将str1 copy到 str2
// output: Hey
// Hey
// Hey
#include <stdio.h>
#include <string.h>
int main()
{
char str1[] = "Hey", str2[] = "World";
char * string = strcpy(str1, str2);
puts(str1);
puts(str2);
puts(string);
return 0;
}
// 将str2 copy到 str1
// output:
// Worl$���
// World
// Worl$���
#include <stdio.h>
#include <string.h>
int main()
{
char str1[] = "Hey", str2[] = "World";
char * string = strcpy(str1, str2+2);
// 参数加2
// 只覆盖数组一部分
puts(str1);
puts(str2);
puts(string);
return 0;
}
// output:
// rld
// World
// rld
strncpy
strncpy(char *, char *, n) | 说明 |
---|---|
其他规则与strcpy一致 | |
第3个参数指定了允许拷贝的最多字符数,当拷贝了n个字符或提前遇到空字符时停止 |
#include <stdio.h>
#include <string.h>
int main()
{
char str1[] = "Hey", str2[] = "World";
char * string = strncpy(str1, str2, 2);
// 限定只能替换2个字符
puts(str1);
puts(str2);
puts(string);
return 0;
}
/* output:
Woy
World
Woy
*/
strcmp
strcmp(char *, char *) | 说明 |
---|---|
比较两个字符串的内容,如果相等返回 0 | |
如果第1个字符串的ASCII码值小于第2个字符串的ASCII码值,返回-1 | |
如果第1个字符串的ASCII码值大于第2个字符串的ASCII码值,返回1 |
strncmp
strncmp(char *, char *, n) | 说明 |
---|---|
其他规则与strcmp函数一致 | |
函数返回在两个字符串的ASCII码值的差 | |
第3个参数指定了比较的字符数,即比较到第n个字符时停止 |
18. 文件
打开文件
FILE * fopen(const char *, const char *); |
---|
第1个参数用于指定打开文件的名称,第2个参数用于指定打开文件的模式
成功打开文件后,函数将返回文件指针,程序可以使用该指针指定这个文件
模式 | 说明 |
---|---|
r | 以只读模式打开文件 |
w | 以只写模式打开文件,把现有文件的长度截为0,如果文件不存在,则创建一个新文件 |
a | 以只写模式打开文件,从现有文件末尾追加内容,如果文件不存在,则创建一个新文件 |
r+ | 以更新模式打开文件(读、写) |
w+ | 以更新模式打开文件(读、写),把现有文件的长度截为0,如果文件不存在,则创建一个新文件 |
a+ | 以更新模式打开文件(读、写),从现有文件末尾追加内容,如果文件不存在,则创建一个新文件 |
rb,wb,ab,rb+,wb+,ab+ | 与前面模式类似,但是是以二进制模式打开文件 |
wx,wbx,w+x,wb+x | 与前面模式类似,但是如果文件已存在或者以独占模式打开文件,则打开文件失败(C11) |
关闭文件
int fclose(FILE *); |
---|
用于关闭该参数所指定的文件,如果成功关闭文件返回0,否则返回EOF
终止程序
void exit( int ); |
---|
- 用于关闭所有打开的文件并结束程序
- 传入EXIT_SUCCESS表明程序执行成功,传入EXIT_FAILURE表明程序执行失败
写入及读取文件
写入
int fputc(int, FILE *); |
---|
第1个参数指定写入的字符,第2个参数指定待写入的文件
如果写入操作成功,就返回已经写入的字符,否则返回EOF
读取
int fgetc(FILE *); |
---|
参数用于指定待读取的文件,若读取成功返回读取到的字符,否则返回EOF
写入及读取字符串
写入
用fputs,第2个参数改为要写入的文件
读取
char * fgets(char *, int, FILE *); |
---|
参数3指定待读取的文件,参数2指定待读取的最多字符数,读取结果存入参数1,成功返回1,否则返回NULL
格式化文件的输入与输出
输出
int fprintf(FILE *,const char *, ...); |
---|
与printf用法相同,第一个参数指定待输出文件
输入
int fscanf(FILE *, const char *, ...); |
---|
与scanf一样,第一个参数指定了待输入文件
写入及读取二进制的文件
函数
size_t fread(void *, size_t, size_t, FILE *); |
---|
size_t fwrite(const void *, size_t, size_t, FILE *); |
size_t 是根据标准C类型定义的类型 |
---|
size_t 是sizeof运算符返回的类型 |
size_t 通常被定义为 unsigned int |
size_t 的转换说明符和整形一起用"%zd" |
说明
参数1是待读取 / 写的数据的地址 |
---|
参数2是待读取 / 写入的每项的大小 |
参数3是待读取 / 写入的数据项数 |
参数4是待读取 / 写入的文件 |
19. 结构体
定义结构体类型
struct person{
char name[50];
char gender;
int age;
};
结构是有由多个成员组成的,大括号内包含的是结构的成员列表 |
---|
结构的成员可以是任意的一种数据类型,甚至可以是其他的结构类型 |
结构的定义可以放在一个函数内部,也可以放在所有函数之外 |
结尾处分号不能省略 ; |
声明结构体变量
根据已定义结构声明变量
struct person p1, p2, ...; |
---|
#include <stdio.h>
struct person{
char name[50];
char gender;
int age;
};
int main()
{
struct person one = {"jackson", 'M', 18};
printf("%s %c %d", one.name, one.gender, one.age);
return 0;
}
#include <stdio.h>
// C99以上才可以使用
struct person{
char name[50];
char gender;
int age;
};
int main()
{
struct person one = {.name="jackson", .gender='M', .age=18};
printf("%s %c %d", one.name, one.gender, one.age);
return 0;
}
定义结构的同时声明变量
struct person {...} p1, p2, ...; |
---|
#include <stdio.h>
struct person{
char name[50];
char gender;
int age;
}one, two;
int main()
{
scanf("%s %c %d", one.name, &one.gender, &one.age);
// 因为字符数组就是地址所以不用&
printf("%s %c %d", one.name, one.gender, one.age);
return 0;
}
通过匿名的结构声明变量
struct {...} p1, p2, ...; |
---|
#include <stdio.h>
struct{
char name[50];
double price;
}book;
int main()
{
scanf("%s %lf", book.name, &book.price);
// scanf输入double要用lf
printf("%s %f", book.name, book.price);
return 0;
}
// input: Cprogarm 66.6
// output: Cprogarm 66.600000
第一种方式是对第二种方式的简化 |
---|
第三种方式定义的结构不能复用 |
通常都是用第一种方式声明结构体变量 |
初始化并访问结构
struct person p = {"John", 'M', 22}; |
---|
struct person p = {.age = 23, .name = "Lily"}; |
访问结构成员
p.name, p.gender, p.age
其中,点(.)是结构体成员运算符,专门用来访问结构体中的成员 |
---|
它具有最高优先级,因此可以写出诸如“&p.gender”这样的表达式 |
注:在scanf输入时字符串不需要用&,因为它本身就是地址
结构数组
#include <stdio.h>
struct person{
char name[50];
char gender;
int age;
};
int main()
{
struct person people[3] = {
{"Jhon", 'M', 22},
{"Alix", 'F', 18},
{"Eson", 'M', 30}
};
for (int i = 0; i < 3; ++i) {
printf("%s %c %d\n", people[i].name, people[i].gender, people[i].age);
// 也可以用下面方式
// struct person p = people[i];
// printf("%s %c %d\n", p.name, p.gender, p.age);
}
return 0;
}
嵌套结构体
#include <stdio.h>
struct person{
char name[50];
char gender;
int age;
};
struct Book{
char name[50];
double price;
struct person author;
};
int main()
{
struct Book book1 = {"The C Progarmming", 99.9, {"None", 'M', 0}};
printf("%s %.2f\nauthor:%s %c %d", book1.name, book1.price, book1.author.name, book1.author.gender, book1.author.age);
return 0;
}
// output:The C Progarmming 99.90
// author:None M 0
结构体指针
-> 这个符号用于指向结构体子数据的指针,用来取子数据 |
---|
#include <stdio.h>
struct person{
char name[50];
char gender;
int age;
};
struct Book{
char name[50];
double price;
struct person author;
};
int main()
{
struct Book book1 = {"The C Progarmming", 99.9, {"None", 'M', 0}};
struct Book * p = &book1;
// 指针变量才能用 ->
printf("\"%s\" %.2f\nauthor:%s %c %d", p->name, p->price, p->author.name, p->author.gender, p->author.age);
return 0;
}
结构体函数
传递结构
void function(struct Book p); |
---|
传递指针
void function(struct Book * p); |
---|
第一种方式传递的是原始结构的副本,会额外消耗内存 |
---|
而且副本被修改了也不会影响到原始数据 |
第二种方式传递的是原始数据的指针,不额外消耗内存 |
如果不想被修改原始数据,可以在结构体指针前加const |
20. typedef
typedef关键字可以为某个类型自定义名称 |
---|
在定义时一般用大写 |
typedef struct person
{
char name[50];
char gender;
int age;
} PERSON;
#include <stdio.h>
int main()
{
typedef struct person {char name[10]; char gender; int age;} PERSON;
PERSON A = {"Jackson", 'M', 18};
printf("%s %c %d\n", A.name, A.gender, A.age);
typedef enum season {spring, summer, autumn, winter} SEASON;
SEASON w = winter;
if (w == 3)
printf("winter\n");
return 0;
}
21. 宏
宏替换
#include PI 3.1415926
| | |
预处理指令 宏 替换体
在预处理阶段,预处理器会将源代码中的宏替换成替换体 |
---|
宏经常用于定义符号常量,也可以表示任何形式的字符串 |
宏定义还可以包含其他宏,但不会替换双引号中的内容 |
宏的参数
#define SQUARE(x) x*x
SQUARE(2) == 2*2
#define SQUAREPF printf("The square of" #x "is %d\n", SQUARE(x))
// #x #号连接前后两个字符串,x是传进来的实参
// 如果定义行太长可以用反斜杠 “\” 链接
在定义宏的时候可以增加参数,并将这些参数用在替换体中 |
---|
这些参数需要定义在圆括号中,所有这样的宏看着像函数 |
使用宏比函数复杂一些,稍有不慎就可能会产生奇怪的作用 |
宏需要消耗更多的内存空间,而函数需要花费更多的运行时间 |
可变参数
把宏参数列表中最后的参数写成省略号 ... |
---|
在替换体中使用预定义宏 ( __ VA_ARGS__ ) 表示省略号代表的内容 |
#define PF(...) printf(__VA_ARGS__)
PF("Hello Wrold\n");
// 省略号... 一定一定要放在最后面
宏 | 含义 |
---|---|
__ DATE __ | 预处理的日期 |
__ TIME __ | 翻译代码的时间 |
__ FILE __ | 表示当前源代码文件名的字符串字面量 |
__ STDC __ | 表示源代码文件中行号的整形常量 |
__ STDC _ HOSTED __ | 设置为1,表示实现遵循C标准 |
下斜杠和字之间没有空格 |
// 简单用法演示
#include <stdio.h>
int main()
{
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
printf("%s\n", __FILE__);
return 0;
}
包含文件
#include 指令用于包含外部文件的内容,预处理器会把#include 指定的文件的内容拷贝到当前文件中,这些内容会输入到#include 指令所在的位置,替换原来的#include 指令
#include 指令有两种形式 |
---|
#include <stdio.h> 在系统目录中查找该文件 |
#include "any" 先在当前目录查找该文件,若未找到再查找系统目录 |
- C语言习惯用".h"后缀表示头文件
- 头文件用于定于可执行代码所依赖的内容,而不是可执行代码本身
条件编译
方式一:
#ifdef xxx
#include "xxx" // 若已经定义了xxx,执行该指令
#else
#include "xxx" // 若未定义xxx,执行该指令
#endif
#ifndef xxx
#include "xxx" // 若未定义了xxx,执行该指令
#else
#include "xxx" // 若已经定义xxx,执行该指令
#endif
// ifdef和ifndef用法类似,但是他们逻辑相反
// ifndef用于判断后面的标识符是否未定义
方式二:
#if xxx == 0
#include "xxx"
#elif xxx == 1
#include "xxx"
#else
#include "xxx"
#endif
// #if、#elif后面需要是常量表达式
常见的应用场景
防止重复包含同一文件
// 定义头文件
#include COMMON_H
#define COMMON_H
... // n
#endif
22. 存储类别
C语言提供了多种不同的存储类别(模型)在内存中存储数据
且需要通过作用域、链接、存储期这3个概念来描述存储类别
- 作用域、链接用于描述一个变量可以在程序中的哪些部分使用
- 存储期用于描述一份数据在内存中可以保留(存活)多久
作用域
一个变量可以是块作用域、文件作用域、函数作用域、函数原型作用域
- 定义在块中的变量具有块作用域(局部),可见范围是从定义处到包含该定义的块的末尾
- 定义在函数外面的变量具有文件作用域(全局),可见范围是从定义处到所在文件的末尾
- goto语句的标签具有函数作用域,任何位置的标签其可见范围都延伸到整个函数
- 函数原型中的参数具有函数原型作用域,可见范围是从参数定义处到原型声明结束
链接
C语言中的变量有3种链接属性:无链接、外部链接、内部链接
- 具有块作用域、函数作用域、函数原型作用域的变量都是无链接变量
- 具有文件作用域的变量,可以是外部链接,也可以是内部链接
- 外部链接变量可以在多个文件中使用,内部链接变量只能在一个翻译单元(源代码文件和它包含的头文件)中使用
extern
在两个不同的源文件中使用全局变量或函数时用extern声明
alpha.c 源文件下
int m = 1; // 外部链接
static int n = 2; // 内部链接
void func() {...}
beta.c 源文件下
extern int m; // 声明外部变量
void func(){
printf("m is %d\n", m); // 使用外部变量
}
static
声明静态存储期(静态变量)
static修饰函数里的变量时,当函数结束是由static修饰的变量不会被释放掉 |
---|
static修饰函数时不能被其他源文件调用 |
static修饰变量时不能被其他源文件调用 |
/static修饰函数里的变量
#include <stdio.h>
int add();
int main()
{
for (int i = 0; i < 5; ++i) {
printf("Now is %d\n", add());
}
return 0;
}
int add()
{
static int m = 1;
return m++;
}
/*
output:
Now is 1
Now is 2
Now is 3
Now is 4
Now is 5
*/
存储期
C中的数据具有4种存储期:静态存储期、自动存储期、线程存储期、动态分配存储期
- 文件作用域变量具有静态存储期,它在程序的执行期间一直存在
- 块作用域变量通常具有自动存储期,即程序进入变量所在的块时为其分配内存,当退出这个块时释放刚才分配的内存。若在该变量前加static修饰,则该变量将具备静态存储期
- 多线程用于解决并发程序的设计,具有线程存储期的数据,从声明到线程结束会一直存在
- 动态分配存储期,是指在调用malloc()函数分配内存,调用free()函数时释放该内存
int m = 1; // 文件作用域,静态存储期,外部链接
int main()
{
{
// 块作用域,自动作用域
int n = 2;
printf("main: m is %d", x);
printf("main: n is %d", x);
}
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)