C基础知识

C基础知识

Hello World

从hello wrld开始

#include 

printf("hello world\n");    
system("pause");

内存

物理角度:内存是计算机中必不可少的一部分,是跟CPU沟通的桥梁,计算机中所有的程序都是运行在内存中。
逻辑角度:内存是一块具备随机访问能力,支持读、写操作,用来存放程序运行中产生的数据的区域。
内存:位(bit),字节(1byte=8bit),KB(1KB=1024字节)MB(1MB=1024KB)
内存编址:计算机中内存按字节编址,每个地址的存储单元可以存放一个字节(8bit)的数据,CPU通过内存地址回去指令和数据,并不关心这个地址所代表的控件具体在什么位置,怎么分布,因为硬件的设计保证一个地址对应着一个具体的空间。
内存地址:通常使用16进制的数值表示,指向内存中某一个区域。

动态内存分配

静态内存分配

int a[1024 * 1024 * 10];

C的内存组成:

  • 运行时系统分配空间:栈,堆
  • 编译时编译器分配的空间:BSS段(存放全局的成员变量),数据段(一段数据),代码段(转化后的汇编指令)

c语言的内存分配

  1. 栈区(stack)自动分配释放(比如window中一般2 M)
  2. 堆区 (heap)手动分配释放(可以占用操作系统%80内存)
  3. 全局区或者静态区
  4. 字符常量区
  5. 程序代码区

动态内存分配

//栈内存
void stackFun(){
    int a[1024]
}
//堆内存
void heapFun(){
    //申请内存空间
    int *p = malloc(1024*1024*4)
    //释放内存
    free(p)
}
//动态内存分配(相当于java中的集合)
int len = 5;
//申请内存,申请完之后,p就变成一个数组
int *p = malloc(len*sizeof(int));
//也可以用calloc(len,sizeof(int));
//给数组赋值
int i = 0;
for(; i<len-1 ;i++){
    p[i] = rand()%100
}
//释放内存
free(p);
p=NULL;

//使用realloc 重新分配内存
//第一个参数:原来指针的内存指针
//第二个参数:内存扩大之后的总大小
int addLen = 5;
int* p2 = realloc(p, sizeof(int) * (len + addLen));

重新分配内存的两种情况

  1. 缩小内存,缩小的那一部分会丢失
  2. 扩大内存,如果当前内存段后面有需要的内存空间,直接扩展这段内存空间,realloc返回原指针
  3. 扩大内存,如果当前内存段后面空闲不够,那么就从堆中找到第一个可以满足内存需求的一块内存,把原来的数据复制过来,并释放原来的内存,返回新的地址
  4. 扩大内存,如果申请失败,返回NULL,原来的指针仍然有效。

内存分配需要注意的几个细节

  1. 不能多次释放
  2. 释放完成之后,给指针置NULL,标志释放完成
  3. 如果不是使用realloc重新赋值而是malloc给p重新赋值之后,在free,并没有真正的释放,会造成内存泄漏。
void main(){
    //给p1赋值
    int* p1 = malloc(1024 * 1024 * 10 * sizeof(int));
    //先释放内存
    free(p1);
    //打印一下可以看到,释放后p1并不为空
    printf("%#x\n",p1);
    p1 = NULL;
    //在给p1重新赋值
    p1 = malloc(1024 * 1024 * 10 * sizeof(int) * 2);
    free(p1);
    p1 = NULL;
}

基本数据类型

int %d               字节数:4
short %d             字节数:2
long %ld             字节数:4(跟java不一样)
float %f             字节数:4
double %lf           字节数:8
char %c              字节数:1
%x 十六进制           
%0 八进制             
%s 字符串

指针

指针存储的是变量的内存地址
指针有类型,地址没有类型
比如int类型的指针不能赋值为double类型的指针,因为指针只是指向一个地址的首位。具体走多少需要看类型。

void main() {
    int i = 100;
    //p是int类型的指针,代表这个int类型的值的内存地址
    int *p = &i;
    printf("%#x\n", p);
    printf("%#x\n", &p);
    printf("%#x\n", &i);
    printf("%#x\n", i);
    //p是指针,*p代表取地址的值
    system("pause");
}

多级指针

指针保存的是变量的地址,它保存的这个地址变量也可以使一个指针变量。

   int a = 50;
//p1上保存的a的地址
int* p1 = &a;
//p2上保存的p1的地址
int** p2 = &p1;
//通过二级指针改变a的值
**p2 = 100;

指针的运算

一般用在数组遍历,指针加一,就是向前移动一个数据类型的字节。比如是int类型的,移动4位,double类型的移动8位

   //数组在内存中是连续存储的
int ids[] = { 78, 90, 23, 65, 19 };
//数组变量名:ids就是数组的首地址
//ids,&ids,&ids[0]的值是一样的
printf("%#x\n",ids);
printf("%#x\n",&ids);
printf("%#x\n",&ids[0]);
//指针变量
int *p = ids;
printf("%d\n",*p);
//指针的加法
//p++向前移动sizeof(数据类型)个字节
p++; 
printf("p的值:%#x\n", p);
//p--;
printf("%d\n", *p);

通过指针给数组赋值

int arr[5];
   int i = 0;
for (; i < 5; i++){
    arr[i] = i;
}

指针数组(数组里面存放的是指针)

int *p[3];

数组指针(行指针)

int (*p)[n]

优先级:()>[]>* ,所以首先p是一个指针,指向一个整型数组,这个数组的长度是n,也可以说是p的步长,也就是说执行p+1的时候,p要跨过n个整型的长度。
当浏览一个图片的时候,可以使用数组指针来读取。

int a[3][4]//定义一个3行4列的二维数组
int (*p)[4]//指针数组指向含有4个元素的以为数组
p=a//将该二维数组的首地址赋值为p,也就是a[0]或者a[0][0]
p++ //执行该语句之后,跨过一行比如从a[0][]指向a[1][]

变量名

变量名是对内存空间上的一段数据的抽象,我们可以对p存的内存地址的变量进行操作
也可以定义一个方法,参数就是一个变量的指针,调用方法的时候,传入指针,就可改变这个变量的值。

void change(int* p){
    *p = 300;
}

void main() {
    int i = 100;
    printf("i的值为:%d\n", i);
    //p是i的指针
    int *p = &i;
    //通过指针赋值
    *p = 200;
    printf("i的值为:%d\n", i);
    //change(p);
    change(&i); 
    system("pause");
}

函数

指针函数 是一个函数
,返回一个指针

//void 是无符号类型,类比于java中的Object
int* int_add_func(void* wParam) {
    printf("指针函数\n");
    int b = 10;
    int *p = &b;
    //指针函数返回一个指针
    return p;
}
void main() {
    int a = 10;
    int_add_func(&a);

    system("pause");
}

函数指针 是一个变量
,是一个指向函数的指针变量。
回调的时候经常用到

//(*funcp)要用括号括起来,括号代表优先级
void(*funcp)(int* a, int* b);
void point_func(int* a, int* b) {
    *a = 200;
    printf("函数指针\n");
}
//main函数中,给这个函数指针赋值,
//**然后就可以通过它调用这个函数了**。
void main() {
    int b = 20;
    funcp = point_func;
    funcp(&a, &b);
    printf("a值 %d", a);

    system("pause");
}
-----------------------------------
int add(int a,int b){
    return a + b;
}
int minus(int a,int b){
    return a - b;
}
void msg(int(*func_p)(int a, int b), int m, int n){
    printf("执行一段代码...\n");
    printf("执行回调函数...\n");
    int r = func_p(m, n);
    printf("执行结果:%d\n",r);
}

void main(){
    //加法
    //msg(add,50,10);
    //减法
    //msg(minus,50,10);
}
//定义两个方法 add,minus。msg这个方法中,需要传入一个
//函数指针int(*func_p)(int a, int b)和两个值
//只要是返回int值,传入两个参数的这种方法,
//都可以传入到msg方法中计算。

字符串

使用字符数组来存储字符串

//'\0'代表结束
char str[] = {'a','b','c','d','e','\0'};
char str[6] = {'a','b','c','d','e'};
char str[10]="china";
str[0] = 's';

字符指针

//内存连续排列
char *str = "hello world";
//不能修改,下一行代码会报错
str[0] = 's';
//使用指针加法,截取字符串
    str += 3;
    while (*str){
        printf("%c",*str);
        str++;
    }
//字符串拼接
void main(void){
    char dest[50];  
    char *a = "china";
    char *b = " is powerful!";
    strcpy(dest, a);
    strcat(dest, b);
    printf("%s\n", dest);

    system("pause");
}
//查找一个字符的位置
void main(void){
    char *str = "I want go to USA!";
    printf("%#x\n", str);
    //U元素的指针
    //str+3
    char* p = strchr(str, 'w');
    if (p){
        printf("索引位置:%d\n", p - str);
    }
    else{
        printf("没有找到");
    }

    system("pause");
}

操作字符串的在线API文档: http://www.kuqin.com/clib/string/strcpy.html

结构体

相当于java中的类。把不同的数据类型整合起来
几种结构体的写法

struct Man {
    char name[20];
    int age;
};
//s1 s2是结构体的变量名
struct student {
    char name[20];
    int age;
} s1 ,s2;
//匿名结构体  相当于单例
struct {
    char name[20];
    int age;
} m1;
//赋值方式如下
void main(){

    struct Man man;
    man.age = 10;
    strcpy(man.name,"chs");
    
    s1.age = 11;
    strcpy(s1.name, "lr");

    m1.age = 12;
    strcpy(m1.name, "czg");

    system("pause");

}

结构体嵌套

//第一种写法
struct student {
    char name[20];
    int age;
} s1 ,s2;
struct teacher {
    char name[20];
    struct student s;
} t;
//第二种写法
struct teacher {
    char name[20];
    struct student {
        char name[20];
        int age;
    } s;
};
//赋值方式
void main(){
    strcpy(t.name, "czg");
    t.s.age = 13;
    strcpy(t.s.name, "cxh");
    system("pause");

}

结构体指针

struct student {
    char name[20];
    int age;
};

void main(){
    struct student s = {"czl",12};
    struct student *p = &s;
    //使用指针赋值
    p->age = 20;
    strcpy(p->name, "xc");
    //使用变量赋值
    s.age = 20;
    strcpy(s.name, "xc");
    system("pause");
}

结构体数组和指针

struct student {
    char name[20];
    int age;
};
void main(){
    struct student stus[] = { {"Jack",20}, {"Rose", 19} };
    //遍历结构体数组
    //第一种方式,使用指针
    struct student *p = stus;
    for (; pname, p->age);
    }
    //第二种方式,使用变量
    int i = 0;
    for (; i < sizeof(stus) / sizeof(struct student); i++) {
        printf("%s,%d\n", stus[i].age, stus[i].name);
    }
    system("pause");

}

结构体的大小

struct Man{
    int age;
    double weight;  
};
void main(){
    struct Man m1 = {20,55.0};
    printf("%#x,%d\n", &m1,sizeof(m1));
    getchar();
}

上面的结构体有两个变量一个int类型一个double类型,通过打印可以看到,该结构体的大小是16。
这就是结构体的内存对齐,结构体的大小,是其内部最大数据类型的整数倍,如果在加一个int类型的变量,那大小就是24。这样做的原因是为了提升效率,以空间换时间。

结构体动态内存分配

struct Man {
    int age;
    char *name;
};
void main(){
    //开辟一块内存
    struct Man *p = (struct Man*)malloc(sizeof(struct Man) * 10);
    //赋值
    struct Man *mp = p;
    mp->age = 18;
    mp->name = "lily";
    //循环遍历
    struct Man *lop = p;
    for (; lop name, lop->age);
    }
    system("pause");

}

typedef 类型取别名

取别名好处:让代码简洁,不同情况下使用不同的别名,不同的名称代表干不同的事情

//Age int类型指针的别名
typedef int Age;
//Age int类型指针的别名
typedef int* Ap;

struct Man{
    char name[20];
    int age;
};
//给结构体取别名
typedef struct Man M;
typedef struct Man* MP;
void main(){
    int i = 5;
    Ap p = &i;

    //结构体变量
    M m1 = {"Rose",20};
    //结构体指针
    MP mp1 = &w1;
    printf("%s,%d\n", m1.name, m1.age);
    printf("%s,%d\n", mp1->name, mp1->age);

    getchar();
}

结构体函数指针成员

Girl了类似于java中的类,sayHi类似于java中的方法。

struct Girl{
    char *name;
    int age;
    //函数指针
    void(*sayHi)(char*);
};

struct Girl {
    char *name;
    int age;
    //函数指针
    void(*sayHi)(char*);
};
void sayHi(char *text) {
    printf(text);
}
void main(){
    struct Girl gl;
    gl.age = 18;
    gl.name = "lily";
    gl.sayHi = sayHi;

    gl.sayHi("hello");
    
    system("pause");
}

给Gril类取别名。在c中大多数情况下都是操作的指针

typedef struct Girl {
    char *name;
    int age;
    //函数指针
    void(*sayHi)(char*);
} Girl;
//定义一个Girl的指针类型
typedef Girl *GirlP;
void sayHi(char *text) {
    printf(text);
}
//改名方法需要传入指针类型
void rename(GirlP gp1) {
    gp1->name = "Lily";
}
void main(){

    Girl gl;
    gl.age = 18;
    gl.name = "lily";
    gl.sayHi = sayHi;

    gl.sayHi("hello");
    //拿到指针
    GirlP gpl = &gl;
    //传入指针改名。使用变量是无法改名的。
    rename(gpl);

    system("pause");
}

共用体(联合体)

共用体是一种特殊的数据类型,允许相同的内存位置存储不同的数据类型,比如定义一个多成员的共用体,它同一个时间只能有一个成员有值。
目的就是为了节省内存,共用体的大小取决于最大的类型的大小。

union  MyValue{
    int x;

    short y;

    double z;
};
void main(){

    union MyValue d1;

    d1.x = 90;

    d1.y = 100; 

    d1.z = 23.8;//最后一次赋值有效

    printf("%d,%d,%lf\n", d1.x, d1.y, d1.z);

    system("pause");

}

上面的例子通过打印之后看到,只有最后一个d1.z有值。

枚举

enum Day
{
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
};

void main() {
    //枚举的值,必须是括号中的值
    enum Day d = Monday;
    printf("%#x,%d\n", &d, d);
    getchar();
}

c中的文件操作

读取文件

void main() {
    char *path = "C:\\Users\\83734\\Desktop\\2.1.txt";
    //r代表只读
    FILE *fp = fopen(path, "r");
    if (fp == NULL) {
        printf("文件打开失败...");
        return;
    }
    //缓冲
    char buff[50];
    while (fgets(buff, 50, fp)) {
        printf("%s", buff);
    }
    fclose(fp);

    system("pause");
}

写入文件

void main() {
    char *path = "C:\\Users\\83734\\Desktop\\3.1.txt";
    //打开  w代表写
    FILE *fp = fopen(path, "w");
    char *text = "你好 世界";
    fputs(text, fp);

    //关闭流
    fclose(fp);

    system("pause");
}

读取二进制文件并复制

void main() {
    char *path = "C:\\Users\\83734\\Desktop\\color_filter.jpg";
    char *path_new = "C:\\Users\\83734\\Desktop\\color_filter_new.jpg";
    //读的指针 rb代表读取二进制
    FILE *read_fp = fopen(path, "rb");
    //写的指针 wb代表写入二进制
    FILE *write_fp = fopen(path_new, "wb");
    //缓冲区
    int buff[50];
    //每次读取到的数据的长度
    int len = 0;
    while ((len = fread(buff,sizeof(int),50,read_fp))!=0) {
        fwrite(buff,sizeof(int),len,write_fp);
    }
    //关闭流
    fclose(read_fp);
    fclose(write_fp);
    system("pause");
}

c读写文本文件和二进制文件的差别,只在回车换行符上面,写文本的时候每遇到一个\n就会将其转换成\r\n,读文本的时候,每遇到一个\r\n就会将其转换成\n。
获取一个文件的大小,可以通过fseek和ftell

void main() {
    char *path = "C:\\Users\\83734\\Desktop\\color_filter.jpg";
    //读的指针 rb代表读取二进制
    FILE *read_fp = fopen(path, "rb");
    //重新定位文件指针
    //SEEK_END文件末尾,0偏移量
    fseek(read_fp, 0, SEEK_END);
    //返回当前的文件指针,相对于文件开头的位移量
    long filesize = ftell(read_fp);
    printf("%d\n", filesize);
    fclose(read_fp);
    system("pause");
}

文件的加解密
可以读取每个文件的字符,然后给每个字符做异或运算,解密的时候在做一次异或运算

//加密
void crpypt(char normal_path[], char crpypt_path[]) {
    //打开文件
    FILE *normal_fp = fopen(normal_path, "r");
    FILE *crypt_fp = fopen(crpypt_path, "w");
    //一次读取一个字符
    int ch;
    while ((ch = fgetc(normal_fp)) != EOF) { //End of File
        //写入(异或运算)
        fputc(ch ^ 3, crypt_fp);
    }
    //关闭
    fclose(crypt_fp);
    fclose(normal_fp);
}
//解密
void decrpypt(char crpypt_path[], char decrpypt_path[]) {
    //打开文件
    FILE *normal_fp = fopen(crpypt_path, "r");
    FILE *crypt_fp = fopen(decrpypt_path, "w");
    //一次读取一个字符
    int ch;
    while ((ch = fgetc(normal_fp)) != EOF) { //End of File
        //写入(异或运算)
        fputc(ch ^ 3, crypt_fp);
    }
    //关闭
    fclose(crypt_fp);
    fclose(normal_fp);
}

void main() {
    char *path = "C:\\Users\\83734\\Desktop\\2.1.txt";
    char *path_c = "C:\\Users\\83734\\Desktop\\2.1.1.txt";
    char *path_de = "C:\\Users\\83734\\Desktop\\2.1.2.txt";

    //crpypt(path,path_c);
    decrpypt(path_c, path_de);
}

前面的加解密都是跟一个3异或,有时候我们可以使用一个字符串作为密码比如“abcd”,读取到的每一个字符循环跟字符串中的字符异或。

//加密
void crpypt(char normal_path[], char crypt_path[],char password[]){
    //打开文件
    FILE *normal_fp = fopen(normal_path, "rb");
    FILE *crypt_fp = fopen(crypt_path, "wb");
    //一次读取一个字符
    int ch;
    int i = 0; //循环使用密码中的字母进行异或运算
    int pwd_len = strlen(password); //密码的长度
    while ((ch = fgetc(normal_fp)) != EOF){ //End of File
        //写入(异或运算)
        fputc(ch ^ password[i % pwd_len], crypt_fp);
        i++;
    }
    //关闭
    fclose(crypt_fp);
    fclose(normal_fp);
}

//解密
void decrpypt(char crypt_path[], char decrypt_path[],char password[]){
    //打开文件
    FILE *normal_fp = fopen(crypt_path, "rb");
    FILE *crypt_fp = fopen(decrypt_path, "wb");
    //一次读取一个字符
    int ch;
    int i = 0; //循环使用密码中的字母进行异或运算
    int pwd_len = strlen(password); //密码的长度
    while ((ch = fgetc(normal_fp)) != EOF){ //End of File
        //写入(异或运算)
        fputc(ch ^ password[i % pwd_len], crypt_fp);
        i++;
    }
    //关闭
    fclose(crypt_fp);
    fclose(normal_fp);

}

void main(){
    char *normal_path = "C:\\Users\\83734\\Desktop\\color_filter.jpg";
    char *crypt_path = "C:\\Users\\83734\\Desktop\\color_filter_c.jpg";
    char *decrypt_path = "C:\\Users\\83734\\Desktop\\color_filter_de.jpg";

    //crpypt(normal_path, crypt_path,"abcd");
    
    decrpypt(crypt_path, decrypt_path,"abcd");
}

C语言的执行流程

  1. 编译:形成目标代码(.obj)
  2. 连接:将目标代码与c函数库连接合并,形成最终的可执行文件
  3. 执行

预编译阶段,主要为编译做准备工作,完成代码的替换。头文件告诉编译器有这么一个函数,连接器负责到代码中找到这个函数

define(宏定义、宏替换 、预编译指令)

define指令

  1. 定义标示(#ifdef __cplusplus 标识支持C++语法)
  2. 定义常数(#define MAX 100)
  3. 定义宏函数,简化比较麻烦的函数

    void dn_com_jni_read(){
        printf("read\n");
    }
    
    void dn_com_jni_write(){
        printf("write\n");
    }
    //定义宏函数    NAME是参数
    #define jni(NAME) dn_com_jni_##NAME();
    void main(){
        //直接调用定义的宏函数
        jni(write);//替换:dn_com_jni_write();
        getchar();
    }
    

c中的库

库可以通过gcc命令编译

//动态库
gcc -shared -fPIC -o libtest.so test.c 
//静态库
gcc -static -fPIC -o libtest.so test.c

动态库:.so/.dll
静态库:.a/.lib
动态库类似于android中的.jar文件
静态库类似于andorid中的.arr文件