jacksonblog

< 2025年2月 >
26 27 28 29 30 31 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 1
2 3 4 5 6 7 8

统计

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语言标准在描述数组表示法时借助了指针!

  1. *定义arr[i] 的意思就是 (arr + i)
  2. 先在内存中找到arr位置,然后移动 i 个单元,检索存储在那里的值
  3. *不要混淆*(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. 字符串

  1. 用双引号括起来的内容称为字符串字面量,编译器会自动在其末尾追加空字符 \0

  2. 字符串的结束标志是\0, 注: '\0' != '0', '\0' = 0, '0'在ASCII码表里是48

  3. 如果多个字面量之间没有间隔,或者只有空白字符分隔,C会将其视为串联起来的字面量 example:“Hello” “World”

  4. 字面量被视为指向该字符串储存位置的指针

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 是一个数组,该数组包含三个指针元素,每个元素都指向一个字符串


字符串的输入与输出

输入字符串

  1. 先分配储存空间(数组表示法),再读取输入的字符串
  2. C语言提供了多个输入字符串的函数,如 gets(), fgets(), scanf() ...

输出字符串

  1. 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 );
  1. 用于关闭所有打开的文件并结束程序
  2. 传入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;
}

posted on   最强CV工程师  阅读(130)  评论(0编辑  收藏  举报

相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示