c语言笔记

C语言学习笔记

1、初识

  • 打印类型大小:
printf("%zu\n",sizeof(char));
  • 关于const:

const修饰的变量,虽然值不可变,但本身还是变量。不能用于数组初始化大小。const int n = 10; arr[n] = {0}; 会报错。

1、枚举

enum Color{
    RED,
    GREEN,
    BLUE
};

int main(){
    enum Color c = RED:
    
    return 0;
}

2、字符串和字符数组

char arr1[] = "abcdef";
//实际上会多存一位,当做结尾标志  `\0`
char arr2[] = {'a','b','c','d','e','f'};
//就是6个空间,纯粹的字符数组
printf("%d\n",strlen(arr1));//求字符串长度 

3、关键字

enum //枚举
struct //结构体
union  //联合体(共用体)
    
extern //声明外部符号的

register //寄存器
    
typedef  //类型重命名
    
volatile

3.1 typedef

//将unsigned int 重命名为uint_32
typedef unsigned int uint_32;

//结构体命名
typedef struct Node{
    int data;
    struct Node* next;
}Node;

int main(){
    uint_32 num1 = 1;
    Node n1;
    return 0;
}

3.2 static

3.2.1 调试技巧

VS2019 调试技巧:CRTL + F10,逐句调试。CRTL + F11,进入函数。

右键(反编译):可以查看当前语句是否有编译。

3.2.2 static 修饰局部变量

普通的局部变量,会存储在栈区。

static修饰的局部变量,会存储在静态区,生命周期直到程序停止。提前初始化过了,只会初始化一次。

3.2.3 static 修饰全局变量
//全局变量
int g_val = 2022;

//函数
int Add(int x,int y){
    return x+y;
}
//声明外部符号
extern int g_val;

//声明外部函数
extern int Add(int x,int y);

int main(){
    printf("%d\n",g_val);
    printf("%d\n",Add(1,2));
    
    return 0;
}

普通的全局变量,可以被外部的 .c 程序通过 extern 获取。

static int g_val = 2022;

被 static 修饰的全局变量,会修改全局变量的外部链接属性,变成内部链接属性。也就是无法被外部 .c 程序获取。

3.2.5 static修饰函数

跟static修饰全局变量无法被外部 .c 程序获取。


3.3 register

register int num = 3;

register 只是建议把该变量放在寄存器中,实际上还是由编译器决定的。当前编译器会智能地把当前的某些变量塞到寄存器中。

4、#define 定义常量和宏

#define MAX 100

#define ADD(x,y) ((x) + (y))

int main(){
    int n = MAX;
    int arr[MAX] = {0};
    
    int c = ADD(10,20);
}

5、指针

指针是地址,指针变量是存放地址的变量。指针变量的空间:32位机器上的是4个字节,64位机器上的是8个字节。

6、结构体

struct Stu{
    ...
}

void print(struct Stu* ps){
	printf("%s",(*ps).name);
    
    printf("%s",ps->name);
}

int main(){
    struct Stu s = {"zhangsan",20,"nan","15596668862"};
    
    printf("%s",s.name);
    print(&s);
}

2、初阶

1、scanf/getchar/putchar

int main(){
    int ch = 0;    //getchar()的返回值是int 型
    while((ch = getchar()) != EOF){
        //getchar 能够获取任意字符,包括 '\n'
        putchar(ch);
    }
    return 0;
}

清空之前的缓冲区:

scanf("%s, password");

//因为scanf读取字符串会遇到空格停止。但是可能空格后面的缓冲区还有内容
//所以为了排除上面输入的干扰,即清除换行前的缓冲区
int ch = 0;
while((ch = getchar()) != '\n'){
    ;
}

2、生成随机数

#include
#include

srand((unsigned int)time(NULL));
//生成随机数
int ret = rand() % 100 + 1;//[1,100]

3、system

system("cls"); //清空控制台
system("shutdown -s -t 60");//60秒关机
system("shutdown -a");//取消关机

4、字符串函数

  1. strcmp(str1,str2):当长度和内容都相等时,返回0;
  2. strcpy(arr1,arr2):将 arr2 复制到 arr1 。
strcpy(ps->name, "zhangsan");//可以用于给字符数组设置值。

5、实参和形参

数组传参实际上传递的是数组首元素的地址,而不是整个数组。所以在函数内部计算一个函数参数部分的数组的元素的个数是不靠谱的。正确操作应该是在外部给函数传入数组长度。

6、函数的声明和定义

函数的声明放在 .h 头文件里。函数的定义放在 .c文件里。要使用该函数只要在相应的 .c 程序里引用头文件。

为了隐藏代码实际实现细节,只暴露函数声明.h文件和静态库给使用者。VS里【项目】->【属性】->【常规】->【配置类型】选择静态库(.lib)。然后运行,生成.lib文件。静态库是二进制数据,看不出内容。

使用方法如下:

#include "add.h"

//导入静态库的方法
#pragma comment(lib, "add.lib")

int main(){
    Add(10,20);
}

头文件:

#pragma once
//防止头文件被重复包含

//函数声明
int Add(int x,int y);

7、递归

7.1 如何解决stack overflow(栈溢出)

  1. 将递归改写成非递归;

  2. 使用static对象替代nonstatic局部对象。

    可以减少每次递归调用和返回时产生和释放nonstatic对象的开销,而且static对象还可以保存递归调用的中间状态,并且可为各个调用层所访问。

8、数组

8.1 一维数组

  • 关于变长数组

arr[n]。用变量定义数组大小,要C99标准后才支持。这种数组不能初始化。

  • 求数组大小
int sz = sizeof(arr) / sizeof(arr[0]);

8.2 二维数组

  • 二维数组的初始化
int arr[3][4] = {1,2,3,4,2,3,4,5,3,4};
//效果是:按行依次排列,不够的用0代替。
//1 2 3 4
//2 3 4 5
//3 4 0 0
int arr[3][4] = {{1,2},{3,4},{5,6}};
//效果是:按行分组
//1 2 0 0
//3 4 0 0
//5 6 0 0
//只能省略行,不能省略列
int arr[][4] = {{1,2,3,4},{2,3}};
//效果:
//1 2 3 4
//2 3 0 0

8.3 冒泡排序

//不要在函数内部,求数组参数的大小。因为本质上数组参数是首元素的地址
void bubble_sort(int arr[],int sz){
    //趟数
    int i = 0;
    for(i = 0; i < sz-1; i++){
        //一趟冒泡排序
        int j = 0;
        for(j=0; j<sz-i;j++){
            if(arr[j] > arr[j + 1]){
                //交换
                int tmp = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = tmp;
            }
        }
    }
}

8.4 数组名

  • 一维数组的数组名
//数组名确实能表示首元素的地址
//但是有2个例外:
//1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
//2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。

int main(){
    int arr[10] = {0};
    
    printf("%p\n",arr); //arr就是首元素的地址
    printf("%p\n",arr+1);
    
    printf("%p\n",&arr); //数组的地址   
    printf("%p\n",&arr+1);
}
  • 二维数组的数组名

例如: arr[3][4]

arr 表示的是二维数组第一行的地址。也表示二维数组的首地址。

arr +1 会得到第二行的地址。

8.5 求二维数组的行和列

int arr[3][4] = {0};

//求行数
//sizeof(arr)总数组大小
//sizeof(arr[0])第一行数组的大小, arr[0]是第一行的数组名
sizeof(arr) / sizeof(arr[0]);

//求列数
sizeof(arr[0]) / sizeof(arr[0][0]);

9、<< >> & | ^

处理的都是二进制。。。。

>>位右移:有两种方式,一种是算术补位,最左侧补跟符号位一样的数字。一种是逻辑补位,最左侧直接补0。

正数的原码、反码、补码都是一样的。

负数的补码,等于负数的反码+1;负数的反码,等于除符号位全部相反。

int a = 3;
int b = -5;
int c = a & b;
//00000000000000000000000000000011   3的补码
//11111111111111111111111111111011   -5的补码

//00000000000000000000000000000011   答案

//最终的答案都是原码,所以补码计算后得到的负数补码还得计算回原码

异或的骚操作:

//a ^ a = 0
//0 ^ a = a

//3^3^5 = 5
//3^5^3 = 5
//异或操作符支持交换律
//不创建临时变量,实现交换函数
//只适用于整数,而且速度会慢
a = a ^ b;
b = a ^ b;
a = a ^ b;

取反~:

效果上看是,原数正负号交换,然后再减1。例如:3变成-4

因为计算机存储使用的是补码。所以是拿补码全部取反(包括符号位),最后结果还原成原码输出。


10、结构体

结构体传参的时候,还是用指针传好点,不然形参会多拷贝一份对象,占用空间大。

在函数中给结构体参数的属性设置值,必须要用指针的形式,不然只是对形参拷贝的一份数据进行修改。

11、隐式类型转换

整型提升:

通用CPU,一般会把表达式中各种长度小于 int 长度的整型值,都先转换成int 或 unsigned int,然后才能送入CPU去执行运算。

例如:char c = -1;因为 -1 有32位,但char只有8位,就会从低位开始截取8位二进制。但是计算的时候,又得进行整型提升,就是高位补充符号位,补到满足32位。

整型提升的过程,就是高位补充符号位。

12、指针

12.1 指针 加减± 整数

float values[5];
float* vp;

for(vp = &values[0]; vp < &values[5]; ){
    //赋值,++向后移动一位
    *vp++ = 0;
    //相当于	*vp=0; vp++;
}

12.2 指针 减- 指针

int strlen(char* str){
    char* start = str;
    while(*str != '\0'){
        str++;
    }
    return (str - start);
}

12.3 指针关系运算

允许指向数组元素的指针与指向数组最后一个元素后面的那个内存位置的指针比较,但是不允许与指向第一个元素之前的那个内存的指针进行比较。


13、结构体

13.1 结构体创建

struct Peo
{
    char name[20];
    char sex[5];
    int high;
}p2,p3;			//这里的p2,p3也是变量,这里是全局变量

int main(){
	struct Peo p1 = {0};
}

14、调试技巧

14.1 常用快捷键

F5

启动调试,经常用来直接跳到下一个断点处。

F9

创建断点和取消断点。

右击断点,选择条件,可以输入满足什么条件,该断点才会触发。

F10

逐过程。一个过程可以是一次函数调用,或者是一条语句。

F11

逐语句。每次都执行一条语句,但是这个快捷键可以使我们的执行进入函数内部。(这是最常使用的)。(但是进不去库函数)。

CTRL + F5

开始执行不调试。

14.2 调试可以查看的参数

打开调试->窗口,主要可以查看断点、监视、自动窗口、局部变量、调用堆栈、内存、反汇编、寄存器。

void test(int a[]){
    //这里想要监视这种数组名,直接用a,只能看到首元素的值
    //想要看到n个元素值,可以使用 a,10。就可以看到10个数据。
}

int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9,10};
    test(arr);

    return 0;
}

在内存窗口看到的数据,是十六进制的。列选择一行显示字节数。两个数字是一个字节。

调用堆栈,右键选择显示外部代码,可以查看函数调用的顺序。

14.3 离谱bug

  • 因为数组访问越界,导致修改了循环中的变量i,从而造成死循环。

15、优秀的代码

  • 多使用assert断言;
  • 多使用const
  • 代码优化;
#include

//const 修饰指针变量
//1. const放在*左边,意味着p指向的对象不能通过p来改变,但是p指针本身是可以改变的。
//2. const放在*右边,意味着p指向的对象是可以通过p来改变,但是不能修改p指针的值。
//const int* p,限制的是*p;
//int* const p,限制的是p;
//const int* const p,全都不能变。


void my_strcpy(char* dest, const char* src){
    //断言
    //断言可以判断一些条件,高级的报错方式。辅助找bug。
    assert(src != NULL);
    while(*dest++ = *src++){
        ;
    }
}

int my_strlen(const char* str){
    int count = 0;
    assert(str);//如果str为空,即条件为假就报错。
    while(*str != '\0'){
        count++;
        str++;
    }
    return count;
}

16、编程常见的错误

  • 编译型错误(语法错误)

直接看错误提示信息(双击),解决问题。相对来说简单

  • 链接型错误(出现在链接期间)

一般是标识符名不存在或者拼写错误

  • 运行时错误

借助调试,逐步定位问题,最难搞。


3、进阶

1、数据的存储

字符的本质是ASCII码值,是整型。

1.1 大端和小端字节序存储

大端存储(倒着放的)

把一个数据的高位内容存放在低地址处,把低位内容存放在高地址处。

小端存储(正着放的)

把一个数据的高位内容存放在高地址处,把低位内容存放在低地址处。

这里的内容,是按一个字节算的,比如说1是0x00000001,就会分成四个字节的内容,00,00,00,01。

1.2 浮点数

32位:1+8+23

64位:1+11+52

2、指针的进阶

2.1 关于字符指针

char* p = "abcdef";

字符指针只是拥有字符串首字符的地址,输出的时候遇到\0才会结束。跟直接代表一个字符串不太一样。

const char* p1 = "abcdef";

常量字符串会在空间中开辟一份唯一的空间,内容相同,不会新开辟。

二级指针

二级指针存放的是一级指针的地址。

2.2 指针数组

指针数组本质是数组,存的是指针罢了。

int *p[10];//p能和[]结合,int和*结合

2.3 数组指针

数组指针存放的是数组的地址。各种各样的数组,甚至指针数组。

int (*p)[10];//p只能和*结合,自然int和[]结合
//数组名确实能表示首元素的地址
//但是有2个例外:
//1.sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小,单位是字节。
//2.&数组名,这里的数组名表示整个数组,取出的是整个数组的地址。

数组指针比较适合的使用场景

arr数组名是数组首元素的地址。对于二维数组,他的首元素是第一行数组的地址。

void print2(int (*p)[5],int r,int c){
    int i=0;
    for(i=0;i<r;i++){
        //*(p+i)相当于arr[0],第一行的数组名,又是首元素的地址
        //*(*(p+i)+j)就是数值,就是他喵的p[i][j]
    }
}

int arr[3][5]={0};
print2(arr,3,5);//arr是第一行数组的地址

记忆方式:arr会优先和[]结合,然后才是*,除非有()。

2.4 数组参数、指针参数

2.4.1 数组参数
void test(int arr[3][5]);//ok
void test(int arr[][]);//No,因为二维数组不能省略列
void test(int arr[][5]);//ok

void test(int *arr);//No
//二维数组首元素的地址就是第一行的数组的地址。
//一维数组的地址不能只用一级指针表示。
void test(int *arr[5]);//No,
//指针数组,肯定不对。这里需要指针,来当参数。
void test(int (*arr)[5]);//ok,arr是个指针,指针指向int[5]数组。
void test(int **arr);//No,一维数组的地址,放到二维指针中,讲不通
//二级指针存的是一级指针的地址。

int main(){
    int arr[3][5] = {0};
    test(arr);
}
2.4.2 一级指针传参
void print(int *p,int sz){
    
}
int main(){
    int arr[10] = {1,2,3,4,5,6,7,8,9};
    int *p = arr;
    int sz = sizeof(arr)/sizeof(arr[0]);
    print(p,sz);
    return 0;
}

实际上如果从函数参数的角度上看,一级指针,有可能是多种传参情况。

void print(int *p)
{}
int a = 10;
int *ptr = &a;
int arr[10];

print(&a);
print(ptr);
print(arr);
2.4.3 二级指针传参
void test(int **ptr){
    printf("%d\n",**ptr);
}
int main(){
    int n = 10;
    int *p = &n;
    int **pp = &p;
    test(pp);
    test(&p);
    return 0;
}

如果函数的参数是二级指针,可以传什么实参呢?

test(int **p){}

int *p1;
int **p2;
int* arr[10];//指针数组

test(&p1);
test(p2);
test(arr);//一维指针数组的数组名

2.5 函数指针

函数也是有地址的。(&函数名)和(函数名),都是函数的地址。

&Add或者Add

int Add(int x,int y);

//函数指针
int (*pf)(int,int) = &Add;//前面是函数返回值,后面是函数参数类型。

int ret = (*pf)(2,3);//函数指针访问函数
//或者
int ret = pf(2,3);//前面的*是摆设。写多少都行,就是要写在括号里
int Add(int x,int y);

void calc(int (*pf)(int,int)){
    int ret = pf(3,5);
}
int main(){
    calc(Add);
    return 0;
}

signal(int, void(*)(int));

void(*)(int)是函数指针类型。

//建议把函数指针类型起个别名
typedef void(* pf_t)(int);
//把void(*)(int)类型重命名为pf_t

int main(){
    void (* signal(int, void(*)(int) ) )(int);
    //其实是
    pf_t signal(int, pf_t);
}

函数指针的用处1:

int Add(int x,int y){
    return x+y;
}
int sub(int x,int y){
    return x-y;
}
//用于降低代码的冗余
void calc(int (*pf)(int, int)){
    int x = 0;
    int y = 0;
    int ret = 0;
    
    printf("请输入2个操作数:>");
    scanf("%d %d",&x,&y);
    ret = pf(x,y);
    printf("%d\n",ret);
}
int main(){
    //实现计算器功能
    switch(input){
        case 1:
            calc(Add);
            break;
        case 2:
            calc(Sub);
            break;
    }
}
2.5.1 函数指针数组
//函数指针数组,把返回值,参数类型相同的函数放在一起
//比函数指针多个[4]
int (*arr[4])(int,int) = {Add, Sub, Mul, Div};

int (*pfArr[])(int,int) = {0, Add, Sub, Mul, Div};
2.5.2 指向函数指针数组的指针
int (*(*ppfArr)[5])(int, int) = &pfArr;
2.5.3 回调函数

回调函数就是一个通过函数指针调用的函数。


2.6 冒泡排序

2.6.1 不普通冒泡
void bubble_sort(int arr[],int sz){
    int i = 0;
    //趟数
    for(i = 0; i < sz - 1; i++){
        int flag = 1;//假设数组是排好序的,可以提前退出循环
        //一趟冒泡排序的过程
        int j = 0;
        for(j = 0; j < sz-1-i; j++){
            if(arr[j] > arr[j + 1]){
                int temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
                flag = 0;
            }
        }
        if(flag == 1){
            break;
        }
    }
}
2.6.2 qsort
void qsort(
   void *base,//你要排序的数据的起始位置
   size_t number,//待排序的数据元素的个数
   size_t width,//待排序的数据元素的大小(字节)
   int (*cmp)(const void* e1, const void* e2)
    //比较函数
);
//__cdecl  函数调用约定

qsort的使用

//比较2个整型元素
int cmp_int(const void* e1, const void* e2){
    //void* 是无具体类型的指针,可以接受任意类型的地址
    //所以不能解引用操作,也不能+-整数。
    //return (*e1 - *e2);
    return (*(int*)e1 - *(int*)e2);
}
int main(){
    int arr[] = {9,8,7,6,5,4,3,2,1,0};
    int sz = sizeof(arr)/sizeof(arr[0]);
    
    qsort(arr, sz, sizeof(arr[0]), cmp_int);
}
2.6.3 升级冒泡排序
void Swap(char* buf1, char* buf2, int width){
    int i = 0;
    for(i = 0; i<width; i++){
        char temp = *buf1;
        *buf1 = *buf2;
        *buf2 = temp;
        buf1++;
        buf2++;
    }
}
void bubble_sort(void *base,int sz,int width,int (*cmp)(const void* e1, const void* e2)){
    int i = 0;
    //趟数
    for(i = 0; i < sz - 1; i++){
        int flag = 1;//假设数组是排好序的,可以提前退出循环
        //一趟冒泡排序的过程
        int j = 0;
        for(j = 0; j < sz-1-i; j++){
            if(cmp((char*)base+j*width, (char*)base+(j+1)*width)>0){
                //交换
                Swap((char*)base+j*width, (char*)base+(j+1)*width, width);
                flag = 0;
            }
        }
        if(flag == 1){
            break;
        }
    }
}

3、字符串+内存函数

  • 求字符串长度
    • strlen
  • 长度不受限制的字符串函数
    • strcpy
    • strcat
    • strcmp
  • 长度受限制的字符串函数
    • strncpy
    • strncat
    • strncmp
  • 字符串查找
    • strstr
    • strtok
  • 错误信息报告
    • strerror
  • 字符操作
  • 内存操作函数
    • memcpy
    • memmove
    • memset
    • memcmp

3.1 strlen

size_t strlen(
   const char *str
);

strlen的返回值类型是size_t,也就是unsigned int型。拿两个strlen做运算会另外浪费空间。

//模拟strlen
size_t my_strlen(const char* str){
    size_t count = 0;
    assert(str);
    while(*str != '\0'){
        count++;
        str++;
    }
    return count;
}

3.2 strcpy

char *strcpy(
   char *strDestination,
   const char *strSource
);
  • 源字符串必须以’\0’结束。
  • 会将源字符串中的’\0’拷贝到目标空间。
  • 目标空间必须足够大,以确保能存放源字符串。
  • 目标空间必须可变。
char* my_strcpy(char* dest, const char* src){
    assert(dest);
    assert(src);
    //ret记录初始地址,方便返回
    char* ret = dest;
    while(*dest++ = *src++){
        //上面这表达式先赋值,再判断*dest的值,为\0就会停止。
        ;
    }
    
    return ret;
}

3.3 strcat字符串追加

char *strcat(
   char *strDestination,
   const char *strSource
);
char *my_strcat(char *dest, const char *src){
    char* ret = dest;
    assert(dest && src);
    while(*dest != '\0'){
		dest++;
    }
    while(*dest++ = *src++){
        ;
    }
    return  ret;
}
  • 源字符串必须以’\0’结束。
  • 目标空间必须足够大,以确保能存放源字符串。
  • 目标空间必须可变。

3.4 strcmp

字符串比较,不能用==。==比较的是地址。应该比较内容

int strcmp(
   const char *string1,
   const char *string2
);

str1>str2返回值大于0;str1=str2返回值为0;str1

int my_strcmp(const char* str1, const char* str2){
    assert(str1 && str2);
    while(*str1 == *str2){
        if(*str1 == '\0')
            return 0;//相等
        str1++;
        str2++;
    }
    return (*str1 - *str2);
}

3.5 strncpy

char *strncpy(
   char *strDest,
   const char *strSource,
   size_t count
);
char* strncpy (char* dest,const char* source,size_t count){
    char *start = dest;

    //复制
    while (count && (*dest++ = *source++) != '\0')
        count--;
    //多的空间用'\0'填满
    if (count) 
        while (--count)
            *dest++ = '\0';

    return(start);
}

3.6 strncat

char *strncat(
   char *strDest,
   const char *strSource,
   size_t count
);
char* strncat (char* front,const char* back, size_t count)
{
    char *start = front;

    while (*front++)
        ;
    front--;//此处应该是'\0'位置

    while (count--)
        if ((*front++ = *back++) == 0)
            return(start);

    *front = '\0';
    return(start);
}

3.7 strncmp

int strncmp(
   const char *string1,
   const char *string2,
   size_t count
);

3.8 strstr查找子串

如果找不到,会返回一个NULL。

char *strstr(
   const char *str,
   const char *strSearch
); 
char *my_strstr(const char *str1,const char *str2){
    assert(str1 && str2);
    const char* s1 = str1;
    const char* s2 = str2;
    const char* p = str1;
    
    while(*p){
        s1 = p;
        s2 = str2;
        while(*s1 != '\0' && *s2 != '\0' && *s1 == *s2){
            s1++;
            s2++;
        }
        if(*s2 == '\0'){
            return p;
        }
        p++;
    }
    return NULL;
}
⭐️KMP算法

目的:母串的 i 不回退,子串的 j 回退到一个特定的位置。

next[]:保存子串某个位置匹配失败后,回退的位置。

如何求next[]数组??

  1. 不管啥数据,next[0]=-1, next[1] = 0;
  2. 找到两个相等的真子串,一个以下标为0字符开始,另一个以下标 (j-1)字符为结尾的真子串。填入的值 K 就是 当前真子串的长度。

数学推导

前提:已知next[i] = k;

  1. 两个真子串可以表示为 p[0]…p[k-1] = p[x]…p[i-1]。

    因为x = i-k , 也就是p[0]…p[k-1] = p[i-k]…p[i-1]。

  2. 如果p[i] == p[k],

    就得到了p[0]…p[k-1] p[k]= p[i-k]…p[i-1]p[i]

    也就是说next[i+1] = k+1。

  3. 如果p[i] != p[k]。

    一直回退,去找 p[i] == p[k],然后又继续满足next[i+1] = k+1。

    回退的意思是 k = next[k]

Java实现

public static int KMP(String str,String sub,int pos){
    if(str == null || sub == null) return -1;
    int lenStr = str.length();
    int lenSub = sub.length();
    if(lenStr == 0 || lenSub == 0) return -1;
    if(pos < 0 || pos >= lenStr) return -1;
    
    //求next[]数组
    int[] next = new int[lenSub];
    getNext(sub,next);
    
    int i = pos;//遍历主串
    int j = 0;//遍历子串
    
    while(i < lenStr && j < lenSub){
        if(j == -1 || str.charAt(i) == sub.charAt(j)){
            i++;
            j++;
        }else {
            j = next[j];//j有可能回退到-1
        }
    }
    if(j >= lenSub){
        return i-j;
    }
    return -1;
}

public static void getNext(String sub,int[] next){
    next[0] = -1;
    next[1] = 0;
    int i = 2;
    int k = 0;//前一项的k
    //遍历子串
    while(i < sub.length()){
        if(k == -1 || sub.charAt(i-1) == sub.charAt(k)){
            //k == -1说明回退到0位置了。next[i]应该赋值0。
            next[i] = k+1;
            k++;
            i++;
        }else {
            k = next[k];
        }
    }
}

C语言实现

void GetNext(char* sub,int* next,int lenSub){
    next[0] = -1;
    next[1] = 0;
    int i=2;
    int k=0;
    
    while(i < lenSub){
        if(k == -1 || sub[i - 1] == sub[k]){
            next[i] = k + 1;
            i++;
            k++;
        } else {
            k = next[k];
        }
    }
}

int KMP(char* str, char* sub, int pos){
    assert(str != NULL && sub != NULL);
    int lenStr = strlen(str);
    int lenSub = strlen(sub);
    if(lenStr == 0 || lenSub == 0) return -1;
    if(pos < 0 || pos >= lenStr) return -1;
    
    int *next = (int *)malloc(sizeof(int) * lenSub);
    assert(next != NULL);
    
    GetNext(sub,next,lenSub);
    
    int i = pos;
    int j = 0;
    
    while(i < lenStr && j < lenSub){
        if(j == -1 || str[i] == sub[j]){
            i++;
            j++;
        }else {
            j = next[j];
        }
    }
    if(j >= lenSub){
        return i - j;
    }
    return -1;
}

next[]数组优化为nextVal[]

  1. 回退到的位置和当前字符一样,就写回退那个位置的nextval值。
  2. 如果回退到的位置和当前字符不一样,就写当前字符原来的next值
		a	b	c	a	a	b	b	c	a	b	c	a	a	b	d	a	b
next   -1	0	0	0	1	1	2	0	0	1	2	3	4	5	6	0	1
nextVal-1	0	0  -1	1	0	2	0  -1	0	0  -1	1	0	6  -1	0

3.9 strtok切割字符串

char *strtok(
   char *strToken,//需要处理的字符串
   const char *strDelimit//分隔符集合
);
const char* sep = "@.";
char email[] = "[email protected]";
char cp[40] = {0};
//第一截字符串,需要传第一个参数
char* ret = strtok(cp, sep);//123456
//后续的字符串,第一个参数为NULL,会自动从上一回记忆的点开始找
ret = strtok(NULL, sep);//qq
ret = strtok(NUll, sep);//com

//内部可能是用static来保存标记。实现该功能。

//通过循环来分割字符串。
char* ret = NULL;
for(ret = strtok(cp ,sep); ret != NULL; ret = strtok(NULL, sep)){
    printf("%s\n", ret);
}

3.10 strerror

返回错误码对应的错误信息。

需要包含头文件 #include

char * strerror(
   int errnum );
int main(){
    //errno   -c语言设置的一个全局的错误码存放的位置

    FILE* pf = fopen("test.txt", "r");
    if(pf == NULL){
        printf("%s\n", strerror(errno));
        return 1;
    }
    else{
        //
    }
    return 0;
}

3.10. .5 字符分类函数、字符转化函数

字符分类、转化函数可以参考该博客:http://t.csdn.cn/QXFAD

3.11 memcpy

对比:strcpy\strncpy只能拷贝字符串,memcpy叫做内存拷贝函数。

memcpy,负责拷贝两块独立空间中的数据。不建议在同一个上修改。

void *memcpy(
   void *dest,
   const void *src,
   size_t count//字节
);
void *my_memcpy(void *dest,const void *src,size_t count){
    assert(dest && src);
    void* ret = dest;
    
    while(count--){
        *(char*)dest = *(char*)src;
        dest = (char*)dest + 1;
        src = (char*)src + 1;
    }
    
    return ret;
}

3.12 memmove

重叠内存的拷贝。。。。

void *memmove(
   void *dest,
   const void *src,
   size_t count
);
//思路:判断dest拷贝的空间和src拷贝的空间的前后关系,选择相应的拷贝顺序是从前往后,还是从后往前。
void *my_memmove(void *dest,const void *src,size_t count){
    assert(dest && src);
    if(dest < src){
        //前->后
        while(count--){
            *(char*)dest = *(char*)src;
        	dest = (char*)dest + 1;
        	src = (char*)src + 1;
        }
    }else {
        //后-->前
        while(count--){
            *((char*)dest + count) = *((char*)src + count);
        }
    }
    
}

3.13 memset内存设置

void *memset(
   void *dest,//需要设置的空间的起始地址
   int c,//想要设置啥值
   size_t count//数量(单位:字节)
);

3.14 memcmp内存比较(字节)

int memcmp(
   const void *buffer1,
   const void *buffer2,
   size_t count
);

4、自定义类型详解(结构体+枚举+联合)

4.1 结构体

4.1.1 结构体类型的声明
struct Stu{
    char name[20];
    int age;
}s1,s2,a[10],*p;//分号前可以选择创建变量,数组,指针类型

//匿名结构体类型,一次性
struct{
    int age;
}n1,n2;//没有名字,只能靠当场创建变量
4.1.2 结构的自引用
struct Node{
    int data;
    struct Node* next;
};
typedef struct Node{
    int data;
    struct Node* next;
}* linklist;

//相当于
struct Node{
    int data;
    struct Node* next;
};
typedef struct Node* linklist;
4.1.3 结构体变量的定义和初始化
struct Point p1 = {3, 4};
4.1.4 结构体内存对齐
  1. 第一个成员在结构体变量起始的地址处。起始位置偏移量为0。

  2. 其他成员要对齐到(对齐数)的整数倍的地址处。

对齐数 = min(编译器默认的值,该成员大小)

  • vs中默认值为8
  1. 如果有嵌套结构体,该嵌套结构体要从其最大对齐数的整数倍开始。

  2. 结构体总大小为最大对齐数的整数倍。

举个例子

struct S1{
    char c1;//8,1 对齐数是1,第一个成员从0开始放。
    int i;	//8,4 对齐数是4,从4开始放,结束偏移量是7
    char c2;//8,1 对齐数是1,从1的整数倍放,放在7后面,就是8
}//上面已经占9个字节空间了
//结构体总大小 = 4的整数倍 = 这里就是12,要把9罩住

为何要内存对齐???

  1. 平台原因,不是所有的硬件平台都能访问任意地址上的任意数据。
  2. 性能原因,访问未对齐的内存,处理器需要两次内存访问;而对齐的内存访问仅需要一次访问。

设计结构体的建议

让占用空间小的成员尽量集中在一起。

offsetof()
size_t offsetof(
   structName,//结构体名
   memberName//成员名
);
#include

printf("%d\n", offsetof(S1, c1));//能够打印当前成员起始的偏移量

使用宏实现offsetof()

#define OFFSETOF(type, m_name) (size_t)&(((type*)0)->m_name)

//(type*)0  假设0是个地址,然后强制转换成结构体指针
//((type*)0)->m_name 取一个结构体的成员
//&(((type*)0)->m_name) 取成员的地址
//(size_t)地址  强制类型转换成整型

OFFSETOF(struct S,c1);
修改默认对齐数
#pragma pack(8)//设置默认对齐数为8
struct S1{
    //...
};
#pragma pack()//取消设置的默认对齐数,还原为默认
4.1.5 结构体传参

传值调用、传址调用

4.1.6 结构体实现位段(位段的填充&可移植性)
位段
  1. 位段的成员是 int、unsigned int 或 signed int 还有 char。(整型)
  2. 位段的成员名后边有一个冒号和一个数字。
struct A{
    int _a:2;	//分配_a 2个比特位
    int _b:5;	//位段可以用来节省空间
    int _c:10;
    int _d:30;
}
位段的内存分配
  1. 位段开辟空间的方式是一次扩容( int 4个字节,或者 char 1个字节)。如果不够,重复一次扩容。
  2. 位段涉及很多不确定因素,位段是不跨平台的,注重可移植的程序应该避免使用位段。
位段的跨平台问题
  1. int 被当做有符号还是无符号是不确定的;
  2. 位段最大位的数目不能确定。(16位机器最大16, 32位机器最大32)
  3. 位段成员在内存中从左往右,还是从右往左分配是不确定的;
  4. 当后一位位段成员比较大,无法容纳于前一个位段剩余的位时,究竟是抛弃剩余位还是利用剩余位,这是不确定的。
位段的应用

网络里的数据报文


4.2 枚举

4.2.1 枚举类型的定义
enum Day{
    Mon,//0
    Tues,//1
    Wed//2
}

//也可以设置第一个的值,后续是1递增
enum Day{
    Mon=1,//1
    Tues,//2
    Wed//3
}
4.2.2 枚举的优点
  1. 增加代码的可读性和可维护性;
  2. 枚举有类型检查;
  3. 防止命名污染;
  4. 便于调试;
  5. 使用方便,一次定义多个。
4.2.3 枚举的使用

enum Day d = Fri;


4.3 联合

4.3.1 联合类型的定义
//共用一个空间
union Un{
    int a;
    char c;
};

union Un u;//该联合体大小为4个字节
4.3.2 联合的特点

联合的成员共用一块空间,该空间大小为联合内部最大成员的大小。

判断大端小端的新方法

int check_sys(){
	union Un{
		char c;
        int i;
    }u;
    u.i = 1;
    return u.c;//因为共用空间,所以此时u.c应该是u.i的第一个字节
    //小端,返回1
    //大端,返回0
}
4.3.3 联合大小的计算
  • 联合的大小至少是最大成员的大小;
  • 当最大成员的大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
union Un{
    char arr[5];//5,数组得按前面的类型char来对齐,也就是1
    int i;//4
}//大小4*2 = 8 把5包着

5、动态内存管理

5.1 动态内存函数的介绍

动态内存函数的使用,要注意确保得到的空间不是NULL。

5.1.1 malloc和free(开辟和销毁动态内存空间)

free只能释放动态内存函数创建的空间。不是释放任意指针。

void *malloc(
   size_t size//字节
);
#include
#include
#include

int main(){
    //动态分配内存
    //这块内存是在堆区里划分的
    int *p = (int*)malloc(40);
    if(p == NULL){
		printf("%s\n", strerror(errno));
        return 1;
    }
    
    free(p);
    p = NULL;
    
    return 0;
}
5.1.2 calloc
void *calloc(
   size_t number,	//数量
   size_t size		//单个数量的字节大小
);

与malloc的区别是 ,calloc会把申请的空间的每个字节初始化为0。

//开辟10个整型的空间
int* p = (int*)calloc(10, sizeof(int));
5.1.3 realloc调整动态内存
void *realloc(
   void *ptr,
   size_t size
);
  • ptr是要调整的内存地址
  • size调整后新的大小
  • 返回值是调整之后的内存起始位置
  • 这个函数不仅调整了内存空间,还把原来内存中的数据移动到新的空间。
realloc(NULL, 40);//等价于malloc(40);

5.2 常见的动态内存错误

  1. 对NULL指针的解引用操作

    **解决方法:**if判断一下是否为NULL;

  2. 对动态开辟空间的越界访问;

  3. 对非动态开辟内存使用free;

  4. 使用free释放一块动态开辟内存的一部分;

    int* p = (int*)malloc(40);
    
    //判断是否为空
    //。。。省略了哦
    
    p++;
    free(p);//No plz 把人家的地址弄丢了,离大谱!
    
  5. 对同一块动态内存多次释放;

  6. 动态开辟内存忘记释放。(内存泄漏)

5.3 柔性数组

结构体中的最后一个成员允许是未知大小的数组,这就叫做柔性数组成员

特点
  • 结构中的柔性数组成员前面必须至少一个其他成员
  • sizeof 返回的这种结构大小不包括柔性数组的内存
  • 包含柔性数组成员的结构用 malloc () 函数进行的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。
typedef strcut st_type{
    int i;
    //柔性数组成员
    int a[0];//或者写成a[],以防有些编译器报错
}type_a;

//含有柔性数组的结构体,要使用malloc创建变量。
type_a* pt = (type_a*)malloc(sizeof(type_a) + 40);

//访问柔性数组成员
pt->a[i];

//扩容
type_a* pp = (type_a*)realloc(pt, sizeof(type_a) + 80);

//=========================================================
//如果不使用柔性数组,使用int* arr取代
struct S2{
    int i;
    int* arr;
};
struct S2* ps = (strcut S2*)malloc(sizeof(struct S2));
ps->arr = (int*)malloc(40);
int *ptr = (int*)realloc(ps->arr,80);
//想要free这些空间要先内部free,然后外部free。容易造成内存碎片。
好处
  • 方便内存释放(只需释放一整个结构体就可以了。)
  • 有利于访问速度

6、文件操作

打开文件,就会在内存的文件信息区,记录文件的相关信息。

6.1 文件的打开和读写操作

int main(){
    FILE* pf = fopen("test.txt","r+");
    if(pf == NULL){
        printf("%s\n", strerror(errno));
        //或者perror("fopen");
        return 1;
    }
    //写文件
    char i = 0;
    for(i = 'a'; i <= 'z'; i++){
        fputc(i,pf);
    }
    //读文件
    int ch = 0;
    while((ch = fgetc(pf)) != EOF){
        printf("%c",ch);
    }
    //关闭文件
    fclose(pf);
    pf = NULL;
    
    return 0;
}
操作模式 效果 没有该文件
“r” 只读 报错
“w” 只写 要么新建,要么覆盖(会先清空)
“a” 追加 在文件末尾写。没有文件会新建
“rb” 读二进制文件 报错
“r+” 读写 首先它是个读,所以跟 r 一样报错
“a+” 结尾追加,任意位置读 跟a一样
“wb+” 读写二进制文件 跟w一样
功能 函数名 适用于
字符输入 fgetc 所有输入流
字符输出 fputc 所有输出流
文本行输入 fgets 所有输入流
文本行输出 fputs 所有输出流
格式化输入 fscanf 所有输入流
格式化输出 fprintf 所有输出流
二进制输入 fread 文件
二进制输出 fwrote 文件
int fputc ( int character, FILE * stream );
int fgetc ( FILE * stream );
//获取一行,字符串长度为 num-1,或者提前结束
char * fgets ( char * str, int num, FILE * stream );

if ( fgets (mystring, 100, pFile) != NULL )
    puts (mystring);
int fputs ( const char * str, FILE * stream );
int fscanf ( FILE * stream, const char * format, ... );

int main ()
{
    char str [80];
    float f;
    FILE * pFile;

    pFile = fopen ("myfile.txt","w+");
    fprintf (pFile, "%f %s", 3.1416, "PI");
    rewind (pFile);
    fscanf (pFile, "%f", &f);
    fscanf (pFile, "%s", str);
    fclose (pFile);
    printf ("I have read: %f and %s \n",f,str);
    //I have read: 3.141600 and PI
    return 0;
}
int fprintf ( FILE * stream, const char * format, ... );

int main ()
{
    FILE * pFile;
    int n;
    char name [100];

    pFile = fopen ("myfile.txt","w");
    for (n=0 ; n<3 ; n++)
    {
        puts ("please, enter a name: ");
        gets (name);
        fprintf (pFile, "Name %d [%-10.10s]\n",n+1,name);
    }
    /*
    	Name 1 [John      ] 
		Name 2 [Jean-Franc] 
		Name 3 [Yoko      ]
	*/
    fclose (pFile);

    return 0;
}
//fread的返回值是实际读取的个数,count是期望读取的个数
size_t fread ( void * ptr, size_t size, size_t count, FILE * stream );

int main () {
  FILE * pFile;
  long lSize;
  char * buffer;
  size_t result;

  pFile = fopen ( "myfile.bin" , "rb" );
  if (pFile==NULL) {fputs ("File error",stderr); exit (1);}

  // obtain file size:
  fseek (pFile , 0 , SEEK_END);
  lSize = ftell (pFile);
  rewind (pFile);

  // allocate memory to contain the whole file:
  buffer = (char*) malloc (sizeof(char)*lSize);
  if (buffer == NULL) {fputs ("Memory error",stderr); exit (2);}

  // copy the file into the buffer:
  result = fread (buffer,1,lSize,pFile);
  if (result != lSize) {fputs ("Reading error",stderr); exit (3);}

  /* the whole file is now loaded in the memory buffer. */

  // terminate
  fclose (pFile);
  free (buffer);
  return 0;
}
size_t fwrite ( 
    const void * ptr,  	//数据 
    size_t size, 		//单个元素的字节
    size_t count, 		//元素数量
    FILE * stream 		//文件指针
);

int main ()
{
  FILE * pFile;
  char buffer[] = { 'x' , 'y' , 'z' };
  pFile = fopen ("myfile.bin", "wb");
  fwrite (buffer , sizeof(char), sizeof(buffer)/sizeof(char), pFile);
  fclose (pFile);
  return 0;
}

c程序默认打开3个流:

  1. FILE* stdin ----- 标准输入流(键盘)
  2. FILE* stdout----- 标准输出流(屏幕)
  3. FILE* stderr------标准错误流(屏幕)

6.2 对比一组函数

scanf/fcanf/sscanf

printf/fprintf/sprintf

struct S{
    char arr[10];
    int age;
    float score;
};

//把结构体,转化成字符串存到buf中。
sprintf(buf, "%s %d %f", s.arr, s.age, s.score);

//把字符串,转化成结构体
sscanf(buf, "%s %d %f", tmp.arr, &(tmp.age), &(tmp.score));

6.3 文件里定位位置的函数

文件操作的时候,每次操作完,文件指针都会往后移一位。

6.3.1 fseek

根据文件指针的位置和偏移量来定位文件指针。

int fseek ( FILE * stream, long int offset, int origin );
//offset偏移量
//origin起始位置
//SEEK_SET 文件开头;SEEK_CUR 当前位置;
//SEEK_END 文件末尾,最后一个字符的再后一位。
int main ()
{
    FILE * pFile;
    pFile = fopen ( "example.txt" , "wb" );
    fputs ( "This is an apple." , pFile );
    fseek ( pFile , 9 , SEEK_SET );
    fputs ( " sam" , pFile );
    fclose ( pFile );
    return 0;
}
6.3.2 ftell

返回文件指针相对于起始位置的偏移量。

long int ftell ( FILE * stream );

fseek (pFile, 0, SEEK_END);
size = ftell (pFile);
6.3.3 rewind

让文件指针回到起始位置。rewind(pFile)

6.4 文件读取结束的判定

在文件读取过程中,不能用feof函数的返回值直接用来判断文件的是否结束。而是应用于当文件读取结束的时候,判断是读取失败结束,还是遇到文件尾结束。

  1. 文本文件读取是否结束,判断返回值是否为EOF(fgetc),或者NULL(fgets);
  2. 二进制文件的读取结束判断,判断返回值是否小于实际要读的个数。(fread)

文本文件例子

int main(){
    int c;//注意,要处理EOF得用int,不是char
    FILE* fp = fopen("test.txt","r");
    if(!fp){
        perroe("文件打开失败");
        return EXIT_FAILURE;
    }
    //fgetc 当读取失败的时候或者遇到文件结束的时候,都会返回EOF
    while((c = fgetc(fp)) != EOF){
        putchar(c);
    }
    //判断是什么原因结束的
    if(ferror(fp))
        puts("读取错误");
    else if(feof(fp))
        puts("成功到达文件结尾");
    
    fclose(fp);
}

二进制例子

enum { SIZE = 5 };
int main(){
    double a[SIZE] = {1., 2., 3., 4., 5.};
    FILE *fp = fopen("test.bin", "wb");//用二进制模式
    fwrite(a, sizeof *a, SIZE, fp);//写入
    fclose(fp);
    
    double b[SIZE];
    fp = fopen("test.bin","rb");
    size_t ret_code = fread(b, sizeof *b, SIZE, fp);//读
    if(ret_code == SIZE){
        puts("成功读取数组");
        for(int n = 0; n < SIZE; ++n) printf("%f ",b[n]);
        putchar('\n');
    } else {//处理错误
        if(feof(fp))
            printf("到达文件结尾");
        else if(ferror(fp)){
            perror("读取文件错误");
        }
    }
    
    fclose(fp);
}

6.5 文件缓冲区

从内存到磁盘,中间还有个缓冲区,一般缓冲区满了,才会存到磁盘。可以手动刷新缓冲区,提前存。

C语言在操作文件的时候,需要刷新缓冲区fflush(pf);或者在文件操作结束的时候关闭文件(自动刷新缓冲区)。不然会出现读写文件的问题。

7、程序环境和预处理

c语言笔记_第1张图片

预处理详解

7.1 预定义符号
//相当于常量,直接调用。
__FILE__	//进行编译的源文件路径
__LINE__	//文件当前的行号
__DATE__	//文件被编译的日期
__TIME__	//文件被编译的时间
__STDC__	//如果编译器遵循ANSI C,其值为1,否则未定义。 
    
printf("file:%s line:%d\n",__FILE__,__LINE__);
7.2 #define
#define MAX 1000
#define SQUARE(x) ((x)*(x)) //参数形式的宏,建议加括号保证优先级
#define reg register //为register这个关键字,创建一个简短的名字
#define do_forever for(;;)	//用形象的符号替换一种实现
#define CASE break;case	//在写case语句时自动把break写上
//内容太多,用斜杠续行,分几行写
#define DEBUG_PRINT printf("file:%s\tline:%d\t	\
							date:%s\ttime:%s\n",\
							__FILE__,__LIKE__,	\
							__DATE__,__TIME__ )
  1. 宏是替换原则,在预处理中不进行运算。
  2. 宏参数和#define定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  3. 如果宏写在字符串里,是不会被替换的。例如:"SQUARE(x)"是无效的。
7.3 #和##

#的作用

如何把参数插入到字符串中?额,实际上是参数名。

#define PRINT(N) printf("the value of "#N" is %d\n",N)
//宏里,#变量,会把变量名字转换成字符串塞入字符串中。
//效果:int a = 10; PRINT(a);
// printf("the value of a is %d\n",N);

#define PRINT(N,FORMAT) printf("the value of "#N" is " FORMAT"\n",N)
//效果:float f = 3.14f;PRINT(f,"%lf");
// printf("the value of f is %lf\n",N);

##的作用

##可以把位于它两边的符号合成一个符号。

它允许宏定义从分离的文本片段创建标识符。

#define ADD_TO_SUM(num, value) \
	sum##num += value;

ADD_TO_SUM(5, 10);//效果是:给 sum5 这个变量增加10
7.4带副作用的宏参数

当宏参数在定义中出现超过一次的时候,如果参数带有副作用,此时使用该宏的时候,就可能造成更大的危害。

7.5 命名约定

把宏名全部大写

函数名不要全部大写

7.6 命令行定义

许多C的编译器提供了一种能力,允许在命令行中定义符号,用于启动编译过程。比如:同一个源文件,然后里面有一个不知道长度数组。我们可以根据机器内存的不同,编译出数组长度不同的同一程序的不同的版本。

7.7 条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的,因为我们有条件编译指令。

比如说:

调试性的代码,删除可惜,保留又碍事,我们可以选择性编译。

#define __DEBUG__

int main(){
    int i = 0;
    int arr[10] = {0};
    for(i = 0; i < 10; i++){
        arr[i] = i;
#ifdef __DEBUG__   //当有#define __DEBUG__就会运行下面代码
        printf("%d\n", arr[i]);//为了观察数组
#endif	// __DEBUG__
    }
    return 0;
}

常用的条件编译指令:

1.
#if	常量表达式
	//...
#endif
//常量表达式由预处理器求值

2.多个分支的条件编译
#if	常量表达式
    //...
#elif 常量表达式
    //..
#else
    //...
#endif
    
3.判断是否定义
#if defined(symbol)
#ifdef symbol
    
#if !defined(symbol)
#ifndef symbol
    
4.嵌套指令
7.8 文件包含

需要防止头文件多次引用,防止同一库多次编译。

//可能有些编译器不能使用
#pragma once
int Add(int x,int y);

//老式方法
#ifndef __TEST_H__
#define __TEST_H__

int Add(int x,int y);

#endif

你可能感兴趣的:(c语言,笔记)