Android JNI和NDK学习(基础篇):C语言基础

2,940 阅读2分钟

概述

C语言对于Android开发来说还是非常必要的,不管你是要阅读源码,还是想要学习NDK,音视频,性能优化等,都不可避免需要接触到C,而且C语言属于系统级的语言,操作系统内核都有C的身影,所以我今天学习一下C语言,本篇博客作为笔记,以防以后忘记

C简介

C语言最初适用于系统开发工作的,特别是组成操作系统的程序,由于C语言产生的代码运行速度与汇编编写的代码运行速度几乎相同,所以采用C语言作为系统开发语言,下面列举几个使用C的实例

  • 操作系统
  • 文本编辑器
  • 打印机
  • 网络驱动器等

C环境的设置

写在源文件的中代码,使我们人类可以看懂的,他需要编译转换为机器语言,这样CPU可以按照指令执行程序,而C语言可以通过GUN编译器,把源码编译为机器语言

Mac上直接下载Xcode就可以使用GUN编译器,开发工具可以使用CLion或者直接用文本编译器

先写一个HelloWord

#include <stdio.h>
 
int main()
{
   /* 我的第一个 C 程序 */
   printf("第一个c程序Hello, World! \n");
   
   return 0;
}

一个C程序包括

  • 预处理器指令 : 程序的第一行#include <stdio.h>是预处理指令,告诉C编译器实际编译之前需要包括stdio.h文件
  • 函数 : 下一行int main()是主函数,程序从这里开始运行,printf()是程序中另一个可用的函数作用是在屏幕上显示Hello, World!
  • 变量
  • 语句&表达式
  • 注释: /* ... */这就是一个注释
  • return 0;标识此函数结束,并返回0

编译执行C程序

  • 首先打开文本编辑器添加上方代码
  • 保存文件为hello.c
  • 打开命令行,进入文件的文件夹
  • 命令行输入gcc hello.c,编译代码
  • 如果没有错误的话,文件夹就会生成a.out的可执行文件
  • 命令行输入a.out,运行程序
  • 然后屏幕上就可以看到Hello, World!
Last login: Mon Feb 17 14:53:00 on ttys002
L-96FCG8WP-1504:~ renxiaohui$ cd Desktop/
L-96FCG8WP-1504:Desktop renxiaohui$ cd c/
L-96FCG8WP-1504:c renxiaohui$ gcc hello.c 
L-96FCG8WP-1504:c renxiaohui$ a.out 
第一个c程序 Hello, World! 
L-96FCG8WP-1504:c renxiaohui$ 

C语言入门

1 基本语法

标识符

标记符是用来标记变量,函数或者任何用户自定的变量名称,一个标识符以字母A—Z,a-z,或者_下划线开始,后面跟0个或多个字母,数字,下划线

标识符中不允许出现标点字符,比如@%,标识符是区分大小写的

关键字

这些关键字不能作为常量名或者变量名,其他标识符的名称

关键字 说明
continue 结束当前循环,开始下一轮循环
switch 用于开关语句
case 开关语句分支
default 开关语句中的其他分支
break 跳出当前循环
do 循环语句的循环体
while 循环语句的循环条件
if 条件语句
else 条件语句否定分支与if一起使用
for 一种循环语句
goto 无条件跳转语句
return 子程序返回语句,可带参数可不带参数
char 声明字符变量或者返回值类型
double 声明双精度浮点类型对象或函数返回值类型
float 声明浮点型变量或返回值类型
short 声明短整形变量或者返回值类型
int 声明整形变量或者返回值类型
long 声明长整形变量或返回值类型
unsigned 声明无符号类型变量或返回值类型
void 声明函数无返回值或无参数,声明无类型指针
enum 声明枚举类型
static 声明静态变量
auto 自动声明变量
const 定义常量,如果一个变量被const修饰,则它的值不能被改变
extern 声明变量或函数在其他文件或本文件的其他位置定义
register 声明寄存器变量
signed 声明有符号类型的变量
sizeof 计算数据类型或变量的长度
struct 声明结构体类型
union 声明共用体
typedef 给数据类型取别名
volatile 说明变量在执行过程中可以隐含的改变

可以看到70%都是java中有的,学习起来并不是很难

2 数据类型

C中的数据类型可以分为以下几种

类型 描述
基本数据类型 他们是算术类型,包含俩种类型,整数类型和浮点数类型
枚举类型 他们也是算术类型,被用来定义在程序中只能赋予其一定的离散整数值得变量
void 标识没有可用的值
派生类型 他包括指针类型,数组类型,结构类型,共用体类型和函数类型

我们先介绍基本数据类型

整数类型

类型 存储大小 值范围
char 1字节 -128-127或0-255
unsigned char 1字节 0-255
signed char 1字节 -128-127
int 2或4字节 -32,768 到 32,767 或 -2,147,483,648 到 2,147,483,647
unsigned int 2或4字节 0 到 65,535 或 0 到 4,294,967,295
short 2字节 -32,768 到 32,767
unsigned short 2字节 0 到 65,535
long 4字节 -2,147,483,648 到 2,147,483,647
unsigned long 4字节 0 到 4,294,967,295

各类型的储存大小,与系统位数有关,为了得到准确大小可以用sizeof来计算

#include <stdio.h>
#include <limits.h>
int main(){
    printf("int 存储大小 : %lu \n", sizeof(int));
    return 0 ;
}
int 存储大小 : 4 

浮点类型

类型 存储大小 值范围 精度
float 4字节 1.2E-38 到 3.4E+38 6 位小数
double 8字节 2.3E-308 到 1.7E+308 15位小数
long double 16字节 3.4E-4932 到 1.1E+4932 19位小数

void类型

类型 描述
函数返回为空 C 中有各种函数都不返回值,或者您可以说它们返回空。不返回值的函数的返回类型为空。例如 void exit (int status);
函数参数为空 C 中有各种函数不接受任何参数。不带参数的函数可以接受一个 void。例如 int rand(void);
指针指向void 类型为void*指针代表对象的地址,而不是类型,例如,内存分配函数 void *malloc( size_t size ); 返回指向 void 的指针,可以转换为任何数据类型。

3 变量

变量是程序可操作的储存区名称,C中每个变量都有特定的类型,类型决定了变量的大小和布局,该范围内的值都可以储存在内存中

C语言中有基本数据类型的变量,也可以有其他类型的变量,比如数组,指针,枚举,结构体,共用体等

变量的定义

变量的定义就是告诉储存器何处创建变量的储存,以及如何创建变量的储存,变量定义指定数据类型,并包含该类型的一个或多个变量列表

type variable_list;

type必须是一个有效的C数据类型,variable_list为变量的名字,可以有多个

int    i, j, k;
char   c, ch;
float  f, salary;
double d;

上方i,j,k等声明并定义了变量i,j,k

变量可以在声明的时候初始化

type variable_name = value;

例如

extern int d = 3, f = 5;    // d 和 f 的声明与初始化
int d = 3, f = 5;           // 定义并初始化 d 和 f
byte z = 22;                // 定义并初始化 z
char x = 'x';

C中变量的声明

变量的声明,像编辑器保证变量以指定的类型和名称存在,这样编辑器就可以在不知道变量完整细节的情况下,也能进一步编译

变量声明有俩种情况

  • 一种是需要建立储存空间的,例如:int a 在声明的时候就已经建立了存储空间。
  • 另一种是不需要建立储存空间的,通过extern关键字声明变量名而不定义它,例如:extern int a 其中变量 a 可以在别的文件中定义的
  • 除非有extern关键字,其他的都是变量的定义
extern int i; //声明,不是定义
int i; //声明,也是定义

4 常量

常量是固定的值,在程序期间不会改变,这些固定的值又叫做字面量

常量可以是任何基本数据类型,常量在值定义之后不会修改

定义常量

在C中有俩种定义常量的方式

  • 使用#define预处理器
#define identifier value

#include <stdio.h>
#include <limits.h>

#define FFF 10
#define DDD 20
#define HHH '\n'
int main(){
    int a ;
    a =FFF*DDD;
    printf("值,%d",a);
    printf("\n字符串,%c",HHH);

    return 0 ;
}
值,200
字符串,
  • 使用const关键字
const type variable = value;

int main() {
    const int FFF =10;
    const int DDD=20;
    const char HHH='\n';
    int a;
    a = FFF * DDD;
    printf("值,%d", a);
    printf("\n字符串,%c", HHH);

    return 0;
}
值,200
字符串,

5 储存类

储存类定义C中变量,函数的范围和生命周期,这些说明符放在他们所修饰的类型之前,下面列出可用的储存类

  • auto
  • register
  • static
  • extern

auto储存类

auto储存类是所有局部变量默认的储存类

{
   int mount;
   auto int month;
}

上面的两种写法都是一样的,auto只能用在函数内,即只能修饰局部变量

register

用于定义储存在寄存器中而不是RAM的局部变量,这意味着变量的最大大小是寄存器的大小

{
   register int  miles;
}

寄存器只用于需要快速访问的变量,比如计数器

static

编译器在声明周期内保持保持局部变量的值,而不需要每次进入和离开作用域是创建和销毁,因此使用static修饰局部变量,可以函数调用间保持局部变量的值

static也可以用于全局变量,当static修饰全局变量时,变量的作用域会限制在他的本文件中,也就是只有本文件才可以访问(普通的全局变量,使用extern外部声明后任何文件都可以访问)

static函数:和上方的全局变量一样(非静态函数可以在另一个文件中直接引用,甚至不必使用extern声明)

extern

用于提供一个全局变量的引用,全局变量对所有的程序文件都可见

当在A文件定义一个全局变量a,B文件想要使用A文件的变量a,可以在B文件使用extern关键字拿到a的引用

第一个文件

#include <stdio.h>

int count =5;

extern void add();

int main() {
    add();
    return 0;
}

第二个文件

#include <stdio.h>

extern int count;

void add(){
    printf("count=%d\n",count);
}

运行后

bogon:untitled renxiaohui$ gcc text.c tex1.c
bogon:untitled renxiaohui$ a.out 
count=5

6 运算符

同java,不在记录

7 判断

同java

8 循环

同java

9 函数

定义函数

return_type function_name( parameter list )
{
   body of the function
}

一个函数的组成部分

  • 返回值类型:一个函数可以返回一个值,return_type是一个函数返回值的数据类型,有些函数不返回数据,这种情况下return_type的关键字是void
  • 函数名称:函数的实际名称function_name
  • 函数参数:当调用函数是,需要向函数传递一个值,就是参数,参数可以有多个,也可以没有
  • 函数主体:函数内实现逻辑的语句
int Max(int num1,int num2){
    int result;
    if(num1>num2){
        result=num1;
    }else{
        result=num2;
    }
    
    return result;
}

这就是一个函数的简单实例

函数声明

函数的声明会告诉编译器,函数的名称和如何调用函数,函数的实际主体可以单独定义

函数声明包括以下几个部分

return_type function_name( parameter list );

针对上方的函数我们可以声明

int Max(int num1,int num2);

在函数声明中,参数名称并不是很重要,可以省略掉

int Max(int ,int);

当你在一个源文件定义一个函数,在另一个文件调用这个函数时,函数声明是必要的,这种情况下,你需要在调用函数文件的顶部声明函数

调用函数

#include <stdio.h>

int Max(int,int);
int main() {
    int num1 = 100;
    int num2 = 120;
    int rec;
    rec = Max(num1,num2);
    printf("比较结果为%d\n",rec);
    return 0;
}

int Max(int num1, int num2) {
    int result;
    if (num1 > num2) {
        result = num1;
    } else {
        result = num2;
    }

    return result;
}

结果为

比较结果为120

10 C的作用域规则

局部变量

在某个函数块内声明的变量为局部变量,他只能被该函数或者该代码块内的函数语句使用,局部变量外部是不可知的

#include <stdio.h>
 
int main ()
{
  /* 局部变量声明 */
  int a, b;
  int c;
 
  /* 实际初始化 */
  a = 10;
  b = 20;
  c = a + b;
 
  printf ("value of a = %d, b = %d and c = %d\n", a, b, c);
 
  return 0;
}

这里的a,b,c 都是局部变量

全局变量

全局变量定义在函数的外部,通常是程序的顶部,全局变量在整个程序的生命周期内都是有效的,在任意的函数内部可以访问全局变量。

全局变量可以被任意函数访问,也就是说全局变量在声明后在整个程序中都是可用的

局部变量和全局变量名称可以相同,但是在函数内,如果俩个名字相,会使用局部变量,不会使用全局变量

#include <stdio.h>

//全局变量
int a = 10;
int c = 40;
int main(){
    //局部变量
    int a = 20;
    int b =30;
    printf("a=%d\n",a);
    printf("b=%d\n",b);
    printf("c=%d\n",c);
    return 0;
}

输出

a=20
b=30
c=40

全局变量与局部变量的区别

  • 全局变量保存在内存的全局储存区,占用静态的储存单元
  • 局部变量保存在栈中,只有在函数被调用时,才动态的为变量分配储存单元

11 数组

声明数组

type arrayName [ arraySize ];

这是一个一维数组,arraySize必须是大于0的常量,type是任意有效的c数据类型,声明一个含有10个double数据的nums数组

double nums [10];

初始化数组

初始化固定数量的数组

int nums [3] = {10,2,3}

初始化数量不固定的数组

int nums []={1,2,3,4,5}

为某个数组赋值

nums[3] = 6;

访问数组元素

int a = num[3];

实例

#include <stdio.h>


int main(){
    int n[10];
    int i,j;

    for(i=0;i<10;i++){
        n[i]=i*10;
    }

    for (int j = 0; j < 10; ++j) {
        printf("%d = %d\n",j,n[j]);
    }

}

结果

0 = 0
1 = 10
2 = 20
3 = 30
4 = 40
5 = 50
6 = 60
7 = 70
8 = 80
9 = 90

12 枚举

枚举是C语言中的一种基本数据类型

enum&emsp;枚举名&emsp;{枚举元素1,枚举元素2,……};

假如我们定义一周七天,假如不用枚举,用常量

#define MON  1
#define TUE  2
#define WED  3
#define THU  4
#define FRI  5
#define SAT  6
#define SUN  7

假如用枚举

enum  Day{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
}

这样看起来会很简洁

注意:默认第一个成员变量为0,后面的顺序加1,如果第一个是1,后面是2,以此类推

枚举变量的定义

1 先定义枚举类型在定义变量

enum DAY{
  MON=1, TUE, WED, THU, FRI, SAT, SUN
};

enum DAY day;

2 定义枚举类型的同时定义枚举变量

enum DAY
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

3 省去枚举名称直接定义枚举变量

enum
{
      MON=1, TUE, WED, THU, FRI, SAT, SUN
} day;

实例

#include <stdio.h>
enum DAY {
    MON = 1, TUE, WED, THU, FRI, SAT, SUN
};
int main() {
   enum DAY day;
   day=THU;
   printf("enun= %d\n",day);
    return 0;
}

结果

enun= 4

在C语言中,枚举是被当做int或者 unsigned int来处理的,所以按照C语言规范是没有办法遍历枚举的

枚举在switch中使用


#include <stdio.h>
#include <stdlib.h>

enum Color {
    red=1, green, blue
};

int main() {
    enum Color enumColor;

    printf("请选择你喜欢的颜色(1 红色 2 绿色 3 蓝色)");
    scanf("%d",&enumColor);

    switch (enumColor){
        case red:
            printf("你喜欢红色\n");
            break;
        case green:
            printf("你喜欢绿色\n");
            break;
        case blue:
            printf("你喜欢蓝色\n");
            break;
    }
    return 0;
}

结果

请选择你喜欢的颜色(1 红色 2 绿色 3 蓝色)1
你喜欢红色

13 指针

每一个变量都有一个内存位置,每一个内存位置都定义了可使用&符号访问地址,他表示在内存中的一个地址

#include <stdio.h>

int main(){

    int a ;

    int b[10];

    printf("a 的内存地址=%p\n",&a);
    printf("b 的内存地址=%p\n",&b);

    return 0;
}

结果

a 的内存地址=0x7ffee5e086c8
b 的内存地址=0x7ffee5e086d0

什么是指针

指针是一个变量,其值为另一个变量的内存地址,使用指针之前需要对其进行声明

type *var-name;

type是指针的类型,他必须是一个有效的C数据类型,var-name是指针的名称,*用来表示这个变量是指针。

int *aa;//一个整形指针
double  *bb;//一个double类型的指针

如何使用指针

使用指针会频繁的进行如下几个操作,定义一个指针变量,把变量地址赋值给指针,访问指针变量中的可用地址

#include <stdio.h>

int main(){

    int a =20;//定义int值
    int *b;//定义指针

    b=&a;//为指针赋值

    printf("变量的地址值=%p\n",&a);
    printf("指针的值=%p\n",b);
    printf("指针的地址值=%d\n",*b);

    return 0;

}

结果

变量的地址值=0x7ffeef4356f8
指针的值=0x7ffeef4356f8
指针的地址值=20

C中的NULL指针

变量声明的时候,如果没有明确的地址可以赋值,为指针赋值一个NULL是一个良好的习惯,赋值为NULL被称为空指针

int main(){
    int *var = NULL;

    printf("var的地址为=%p\n",var);

    return 0;
}
var的地址为=0x0

指针指向指针

#include <stdio.h>

int main(){
    int a;
    int *b;
    int **c;

    a=40;

    b=&a;

    c=&b;

    printf("a的值为=%d\n",a);

    printf("b的值为=%d\n",*b);

    printf("c的值为=%d\n",**c);

    return 0;
}
a的值为=40
b的值为=40
c的值为=40

传递数组给函数,函数返回数组

#include <stdio.h>

int sum(int *arr,int size);

int* getarr();

int main() {
    int a[4] = {1, 2, 3, 4};
    int v = sum(a,4);
    printf("sum=%d\n",v);

    int *p  = getarr();

    for (int i = 0; i < 4; ++i) {
        printf("数组=%d\n",*(p+i));
        printf("数组=%d\n",p[i]);

    }
    return 0;
}

int sum(int *arr, int size) {
     int sum = 0;
    for (int i = 0; i < size; ++i) {
        printf("i=%d\n", arr[i]);
        printf("i=%d\n", *(arr+i));

        sum+=arr[i];
    }
    return sum;
}

int * getarr(){
    static int arr[4]={2,4,5,7};

    return arr;
}
i=1
i=1
i=2
i=2
i=3
i=3
i=4
i=4
sum=10
数组=2
数组=2
数组=4
数组=4
数组=5
数组=5
数组=7
数组=7

指针运算

C指针用数字表示地址,因此可以进行算术运算,++,--,+,-等,假如prt是一个int类型的地址1000,那么执行prt++,prt将指向1004,即当前位置移动4个字节,假如prt是一个char类型的地址1000,那么执行prt++,prt将指向1001,这个跟类型也是相关的

  • 指针的每一次递增,他实际上会指向下一个元素的储存单元
  • 指针的每一次递减,他实际上指向上一个元素的储存单元
  • 指针在递增和递减跳跃的字节数,取决于指针所指向变量的数据类型的长度,比如int就是4个字节

递增一个指针

#include <stdio.h>

const int Max = 3;

int main() {
    int arr[3] = {1, 2, 3};
    int i ,*p;
    //给p指针赋值数组中第一个元素的地址
    p = arr;

    for (int i = 0; i < Max; ++i) {
        printf("元素的地址=%p\n", p);
        printf("元素的地址=%d\n", *p);
        //移动到下个位置
        p++;
    }
    return 0;
}
元素的地址=0x7ffee165b6ac
元素的地址=1
元素的地址=0x7ffee165b6b0
元素的地址=2
元素的地址=0x7ffee165b6b4
元素的地址=3

指针数组

有一种情况,我们数组内可以存储内存地址值

#include <stdio.h>

const int Max= 3;
int main(){
    int arr[3]={1,2,3};
    int *p[Max];

    for (int i = 0; i <Max ; ++i) {
        p[i]=&arr[i];
    }

    for (int j = 0; j < Max; ++j) {
        printf("指针数组数据=%p\n",p[j]);
        printf("指针数组数据值=%d\n",*p[j]);

    }
    return 0;
}
指针数组数据=0x7ffee7cda6ac
指针数组数据值=1
指针数组数据=0x7ffee7cda6b0
指针数组数据值=2
指针数组数据=0x7ffee7cda6b4
指针数组数据值=3

指向指针的指针

指针也可以指向指针


#include <stdio.h>

int main(){
    int a = 10;
    int *b;
    int **c;

    b=&a;
    c=&b;

    printf("a的值为=%d\n",a);
    printf("b的值为=%d\n",*b);
    printf("c的值为=%d\n",**c);

}
a的值为=10
b的值为=10
c的值为=10

传递指针给函数

#include <stdio.h>
#include <time.h>

void getlong(unsigned long *a);
int main() {
 unsigned long b;

 getlong(&b);

 printf("b的值为=%ld\n",b);
}

void getlong(unsigned long *a) {
    *a = time(NULL);
    return;
}
b的值为=1585048748

14 函数指针和回调函数

函数指针是指向函数的指针变量,函数指针可以向普通函数一样,传递参数,调用函数

函数返回值类型 (* 指针变量名) (函数参数列表);
#include <stdio.h>

int max(int, int);

int main() {
    //p是函数指针
    int (*p)(int, int) = &max;//&可以省略
    int a, b, c, d;

    printf("请输入三个数字:");
    scanf("%d,%d,%d", &a, &b, &c);
    d = p(p(a, b), c);//相当于调用max(max(a,b),c);
    printf("d的值为=%d\n", d);
    return 0;
}

int max(int a, int b) {
    return a > b ? a : b;
}

输出

请输入三个数字:1,2,3
d的值为=3

将指针函数当做参数传递给函数

#include <stdio.h>
#include <stdlib.h>


void setvalue(int *arr, int b, int(*p)(void)) {
    for (int i = 0; i < b; ++i) {
        arr[i] = p();
    }
}

int stett(int(*p)(void)){
    return p();
}

int getvalue(void) {
    return rand();
}

int main() {
    int arr[10];

    setvalue(arr, 10, getvalue);
    for (int i = 0; i < 10; ++i) {
        printf("i=%d\n", arr[i]);
    }

    int b;
    b= stett(getvalue);
    printf("b=%d\n", b);


    return 0;
}


结果

i=16807
i=282475249
i=1622650073
i=984943658
i=1144108930
i=470211272
i=101027544
i=1457850878
i=1458777923
i=2007237709
b=823564440

15 字符串

C语言定义字符串Hello,两种形式,字符串和字符数组的区别:最后一位是否是空字符

#include <stdio.h>

int main(){
    char hello[6]= {'H','e','l','l','o','\0'};
    char hello1[]= "hello";
    char *hello2="hello";

    printf("测试=%s\n",hello);
    printf("测试=%s\n",hello1);
    printf("测试=%s\n",hello2);

    return 0;
}
测试=Hello
测试=hello
测试=hello

16结构体

结构体可以存储不同类型的数据项

定义一个结构体

struct tag { 
    member-list
    member-list 
    member-list  
    ...
} variable-list ;
  • tag:结构体标签
  • member-list:结构体数据项
  • variable-list:结构体变量
struct Book {
    int book_id ;
    char title[50];
    char author[50];
    char subject[50];
} book;

在一般情况下,tag、member-list、variable-list 这 3 部分至少要出现 2 个。以下为实例:

//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//同时又声明了结构体变量s1
//这个结构体并没有标明其标签
struct 
{
    int a;
    char b;
    double c;
} s1;
 
//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE
{
    int a;
    char b;
    double c;
};
//用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;
 
//也可以用typedef创建新类型
typedef struct
{
    int a;
    char b;
    double c; 
} Simple2;
//现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;

结构体中可以含有其他结构体和指针

//结构体包含其他结构体
struct AA{
    int a;
    struct Book b;
};
//结构体包含自己的指针
struct BB{
    int b;
    struct BB *nextbb
};

结构体的初始化

struct Book {
    int book_id ;
    char title[50];
    char author[50];
    char subject[50];
} book= {1,"c语言","小明","bb"};
title : c语言
author: 小明
subject: bb
book_id: 1

访问结构体的成员

访问结构体的成员可以用.符号,比如上方的book.title;

int main(){

    struct Book book1;

    strcpy(book1.title,"学习书");
    strcpy(book1.author,"小红");
    strcpy(book1.subject,"111");

    book1.book_id=222;


    printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book1.title, book1.author, book1.subject, book1.book_id);

    return 0;
}
title : 学习书
author: 小红
subject: 111
book_id: 222

结构体作为函数的参数


void printstuct(struct Book book);
int main(){

    struct Book book1;

    strcpy(book1.title,"学习书");
    strcpy(book1.author,"小红");
    strcpy(book1.subject,"111");

    book1.book_id=222;


    printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book1.title, book1.author, book1.subject, book1.book_id);
    printstuct(book1);
    return 0;
}


void printstuct(struct Book book){
    printf( "Book title : %s\n", book.title);
    printf( "Book author : %s\n", book.author);
    printf( "Book subject : %s\n", book.subject);
    printf( "Book book_id : %d\n", book.book_id);
}
title : 学习书
author: 小红
subject: 111
book_id: 222
Book title : 学习书
Book author : 小红
Book subject : 111
Book book_id : 222

指向结构体的指针

定义,赋值,调用

struct Books *struct_pointer;

struct_pointer = &Book1;

struct_pointer->title;
int main() {

    struct Book book1;

    strcpy(book1.title, "学习书");
    strcpy(book1.author, "小红");
    strcpy(book1.subject, "111");

    book1.book_id = 222;
    printstuct1(&book1);
    return 0;
}

void printstuct1(struct Book *book) {
    printf("Book title : %s\n", book->title);
    printf("Book author : %s\n", book->author);
    printf("Book subject : %s\n", book->subject);
    printf("Book book_id : %d\n", book->book_id);
}
Book title : 学习书
Book author : 小红
Book subject : 111
Book book_id : 222

17 共用体

共用体是一个特殊的数据类型,允许在相同的储存位置,储存不同的数据类型,可以定义一个带有多个成员的共用体,但是任何时候只能一个成员带值

定义共用体

union定义共用体,

union [union tag]
{
   member definition;
   member definition;
   ...
   member definition;
} [one or more union variables];

union tag是可选的,每个member definition;都是标准的变量定义,如int i char b等,在分号之前可以定义一个或多个共用体变量是可选的

定义一个成员有int,float,char[]的共用体


#include <stdio.h>
#include <string.h>


union Data {
    int a;
    char b[100];
    float c;
};

int main() {

    union Data data;

    printf("数据长度=%lu\n", sizeof(data));

    data.a = 1;
    data.c = 10.00;
    strcpy(data.b, "测试数据");
    
    printf("数据%d\n",data.a);
    printf("数据%f\n",data.c);
    printf("数据%s\n",data.b);
  

    return 0;
}
数据长度=100
数据-393497114
数据-5278115000342806695772160.000000
数据测试数据

我们看到数据a,c成员的值有损坏,是因为最后赋值的变量占用了的内存位置,这也是b变量可以正确输出的原因

我们同一时间只能使用一个变量


#include <stdio.h>
#include <string.h>


union Data {
    int a;
    char b[100];
    float c;
};

int main() {

    union Data data;

    printf("数据长度=%lu\n", sizeof(data));

    data.a = 1;
    printf("数据%d\n",data.a);

    data.c = 10.00;
    printf("数据%f\n",data.c);

    strcpy(data.b, "测试数据");
    printf("数据%s\n",data.b);




    return 0;
}
数据长度=100
数据1
数据10.000000
数据测试数据

在这里所有的成员都可以正确输出,是因为同一时间只用到了一个变量

18 typedef

C语言提供typedef关键字,可以使用它为类型起一个新的名字

#include <stdio.h>

typedef unsigned int TEXT;
int main(){
    TEXT a = 11;

    printf("参数为=%d\n",a);
    return 0;
}
参数为=11

也可以为用户自定义的数据类型去一个新的名字,比如结构体

#include <stdio.h>
#include <string.h>

typedef unsigned int TEXT;

typedef struct BOOKS {
    char a[50];
    char b[50];
} Book;

int main() {
    TEXT a = 11;
    printf("参数为=%d\n", a);

    Book book;

    strcpy(book.a, "测试1");
    strcpy(book.b, "测试2");
    printf("a=%s\n", book.a);
    printf("b=%s\n", book.b);


    return 0;
}
参数为=11
a=测试1
b=测试2

typedef和#define 的区别

#define是一个C指令,用于为各种数据类型定义别名,与typedef类似,但是他有以下几种不同

  • typedef仅限于为类型定义符号名称,#define不仅为类型定义别名,也可以为数值定义别名
  • typedef为编译器解释执行,#define为预编译器进行处理
#define TRUE 0
#define FALSE 1

int main() {
    printf("数值为=%d\n", TRUE);
    printf("数值为=%d\n", FALSE);
    return 0;
}
数值为=0
数值为=1

19 输入和输出

getchar() & putchar() 函数

int getchar(void) 函数从屏幕读取下一个可用的字符,并把它返回为一个整数。这个函数在同一个时间内只会读取一个单一的字符。您可以在循环内使用这个方法,以便从屏幕上读取多个字符。

int putchar(int c) 函数把字符输出到屏幕上,并返回相同的字符。这个函数在同一个时间内只会输出一个单一的字符。您可以在循环内使用这个方法,以便在屏幕上输出多个字符。

#include <stdio.h>

int main(){
    printf("请输入一个字符\n");

    int  a = getchar();

    printf("你的输入为\n");

    putchar(a);

    printf("\n");
}
请输入一个字符
text
你的输入为
t

gets() & puts() 函数

char *gets(char *s)函数从stdin读取一行到s指向的缓存区,直到一个终止符或一个EOF int puts(const char *s)函数吧一个字符串s和一个尾随的换行符写入stdout

#include <stdio.h>

int main(){
    char a[100];

    printf("输入你的字符\n");
    gets(a);

    printf("你输入的字符为\n");

    puts(a);
    return 0;
}
输入你的字符
text
你输入的字符为
text

scanf() 和 printf() 函数

int scanf(const char *format, ...)函数从标准输入流 stdin 读取输入,并根据提供的 format 来浏览输入。

int printf(const char *format, ...)函数把输出写入到标准输出流 stdout ,并根据提供的格式产生输出。

format 是一个简单的常量字符串,但是你可以定义%s,%d,%c,%f来读取字符串,数字,字符,浮点数

#include <stdio.h>

int main() {
    char a[100];
    int b;

    printf("输入文字\n");

    scanf("%s%d", a, &b);

    printf("你输入的文字=%s,%d\n",a,b);
    return 0;
}
输入文字
text 123
你输入的文字=text,123

20 文件读写

#include <stdio.h>

int main() {
    FILE *file;

    //打开文件允许读写,如果不存在则创建该文件
    file = fopen("/Users/renxiaohui/Desktop/test.txt", "w+");
    //写入一行
    fprintf(file, "this is a text\n");
    //再写入一行
    fputs("this is a text aaaa\n", file);

    fclose(file);

}

会在创建文件test.txt,并写入俩行文字

#include <stdio.h>

int main() {
    FILE *file1;

    char a[255];
    //打开一个文件,只读
    file1 = fopen("/Users/renxiaohui/Desktop/test.txt", "r");
    //读取文件,当遇到第一个空格或者换行符会停止读取
    fscanf(file1, "%s", a);
    printf("1= %s\n", a);

    //读取字符串到缓冲区,遇到换行符或文件的末尾 EOF返回
    fgets(a, 255, file1);
    printf("2= %s\n", a);

    fgets(a, 255, file1);
    printf("3= %s\n", a);

    //关闭文件  
    fclose(file1);
}

读取刚才创建的文件

1= this
2=  is a text
3= this is a text aaaa

21 预处理器

C预处理器不是编译器的组成部分,他是编译过程中的一个单独步骤,他们会在编译器实际编译之前完成所需的预处理

所有的预处理器都是以(#)开头的,他必须是第一个非空字符,为了增强可读性,预处理器应该从第一列开始,下面列出比较重要的预处理器

指令 描述
#define 定义宏
#undef 取消定义的宏
#include 包含一个源文件代码
#ifdef 如果宏已经定义则返回真
#ifndef 如果宏没有定义则返回真
#if 如果给定条件为真则编译一下代码
#else #if的替代方案
#elseif 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个#if。。#else 语句
#error 遇到标准错误是,输出错误消息
#pragma 使用标准化方法,向编译器发布特殊指令到编译器中去

预编译器实例

#define MAX 20

这个指令表示,把所有的MAX定义为20,使用#define定义常量可以增加可读性

#include <stdio.h>
#include "myheader.h"

第一个表示从系统库获取stdio.h库,并添加到源文件中,一个是从本地目录获取myheader.h,并添加到源文件中

#undef FILE 
#define FILE 99

取消已经定义的FILE,并把它重新定义为99

#ifdef MESSAGE
    #define MESSAGE "you wish"
#endif

这个表示只有MESSAGE未定义时,才定义MESSAGE为you wish

预定义宏

ANSI C中预定义宏,我们可以在编程中使用这些宏,但不可以改变他的值

描述
1__DATE__ 当前日期,一个以 "MMM DD YYYY" 格式表示的字符常量。
1__TIME__ 当前时间,一个以 "HH:MM:SS" 格式表示的字符常量。
1__FILE__ 这会包含当前文件名,一个字符串常量。
1__LINE__ 这会包含当前行号,一个十进制常量。
1__STDC__ 当编译器以 ANSI 标准编译时,则定义为 1。

看下使用

#include <stdio.h>
int main() {

    printf("data=%s\n",__DATE__);
    printf("file=%s\n",__FILE__);
    printf("time=%s\n",__TIME__);
    printf("line=%d\n",__LINE__);
    printf("stdic=%d\n",__STDC__);

    return 0;
}
data=Mar 31 2020
file=text27.c
time=15:31:49
line=19
stdic=1

参数化宏

参数化宏可以模拟函数,例如下面是一个计算数字平方的方法

int square(int x) {
   return x * x;
}

可以用参数化宏来表示

#define square(x) ((x)*(x))

在使用参数化宏之前必须使用#define来定义,参数列表必须包含在圆括号内的,并且必须紧跟在宏名称之后,不允许有空格

#define square(x) ((x)*(x))

int main() {

    printf("平方为=%d\n",square(5));

    return 0;
}
平方为=25

预处理运算符

宏延续运算符(\)

一个宏通常写在一个单行上,但是如果宏太长,一个单行容不下,则使用宏延续运算符(\)例如

#define message_for(a,b) \
            printf(#a"and "#b"we are friend\n")

字符串常量化运算符(#)

在宏定义中,需要把一个宏的参数转化为字符串常量时,则使用字符串常量化运算符(#)例如:

#define message_for(a,b) \
            printf(#a"and "#b"we are friend\n")

int main() {

    message_for("xiaoming","xiaohong");

    return 0;
}
"xiaoming"and "xiaohong"we are friend

** 标记粘贴运算符(##)**

直接看代码比较

#define add(n) printf("token" #n " = %d\n",token##n)


int main() {

    int token34 =40;

    add(34);

    return 0;
}
token34 = 40

这个是怎么发生的,因为这个实际会从编译器中产出以下实际输出

printf ("token34 = %d", token34);

defined()运算符

预处理器defined运算符是在常量表达式中的,用来确定一个标识符是否已经#define定义过,如果定义过为真,如果没有定义过值为假

#if !defined(TEXT)
#define TEXT "text\n"
#endif


int main() {

    printf(TEXT);

    return 0;
}
text

22 头文件

头文件是后缀名为.h的文件,包含了函数声明和宏定义,有俩种头文件系统头文件和c程序员自己编写的头文件

在C程序中建议把所有的常量,宏,系统全局变量,和函数原型写入头文件中

引入头文件

使用预处理命令#include 引入头文件,有俩种类型

#include <file>

这种形式用于引用系统头文件,他在系统目录搜索头文件

#include "file"

这种形式用于引用用户头文件,他在当前的文件目录搜索头文件

只引用一次头文件

如果一个头文件引用被引用俩次,编译器会处理俩次头文件的内容,这将产生错误,为了防止这种情况,标准的做法

#ifndef HEADER_FILE
#define HEADER_FILE

the entire header file file

#endif

如果没有定义HEADER_FILE则定义引入头文件,如果下次在引用因为HEADER_FILE已经定义则不会再次引入

关于.h和.c的区别请看这篇文章

www.cnblogs.com/laojie4321/…

23 错误处理

当C语言发生错误时,大多数的C会返回1或NULL,同时会设置一个错误代码errno,改错误代码是全局变量,表示执行函数期间发生了错误,可以在 errno.h头文件中找到各种错误代码

所以程序员可以通过返回值来判断是否有错误,开发人员在初始化时吧errno初始化为0,是一个良好的编程习惯,0表示没有错误

errno、perror() 和 strerror()

C语言提供了perror() 和 strerror()来显示errno相关的文本信息

  • perror()函数:他将显示你传入的文字后面跟一个冒号和来errno相关的文本信息
  • strerror()函数:返回一个指针,指向当前 errno 值的文本表示形式。
#include <stdio.h>
#include <errno.h>
#include <string.h>

int main() {
    FILE *pf;
    int errnum;
    pf = fopen("aaa.txt", "rb");

    if (pf==NULL){
        //stderr,输出到屏幕
        printf("错误号为%d\n",errno);
        fprintf(stderr,"错误为%s\n",strerror(errno));
        perror("perror显示错误");
    }
    return 0;
}
错误号为2
错误为No such file or directory
perror显示错误: No such file or directory

24 内存管理

C语言为内存的分配提供了几个函数,在<stdlib.h>文件头中可以找到

函数 描述
void *calloc(int num, int size); 在内存中动态的分配num个长度为size的连续空间,并将每一个字节初始化为0
void free(void *address); 该函数释放address所指向的内存块,释放的是动态分配的内存空间
void *malloc(int num); 在堆区分配一块指定大小的内存空间,用来储存数据,该内存空间在函数执行完之后不会初始化,他们的值是位置的
void *realloc(void *address, int newsize); 该函数重新分配内存,新内存分配到newsize大小

注意:void* 表示未确定类型的指针,void*类型指针可以通过强制转换转换为任意其他类型的指针

动态分配内存

编程时如果你预先知道数组的大小,那么定义数组时会比较容易,比如一个可以容纳100个字符的数组

char name[100];

但是如果不知道需要储存文本长度的,那就需要动态分配内存了

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    char a[100];
    char *b;

    strcpy(a, "固定数组100");

    //动态分配内存
    b = (char *) malloc(200 * sizeof(char));
//    b= calloc(200, sizeof(char));
    if (b == NULL) {
        printf("分配内存失败");
    } else {
        strcpy(b, "动态分配内存成功");
    }
    printf("值为=%s\n",b);
    //重新分配内存
    b = realloc(b, 500 * sizeof(char));
    if (b == NULL) {
        printf("分配内存失败");
    } else {
        strcpy(b, "重新分配内存成功");
    }

    printf("值为=%s\n",b);

    //释放内存
    free(b);
    return 0;
}
值为=动态分配内存成功
值为=重新分配内存成功

上面分配内存的代码也可以替换为

    b= calloc(200, sizeof(char));

25 命令行参数

执行程序时可以从命令行传递参数给程序,这些值为命令行参数,命令行参数是通过main函数参数来处理的,其中,argc 是指传入参数的个数,argv[] 是一个指针数组,指向传递给程序的每个参数

#include <stdio.h>

int main(int argc, char *argv[]) {
    if (argc == 2) {
        printf("参数为 %s\n", argv[1]);
    } else if (argc > 2) {
        printf("参数超过俩个\n");
    } else {
        printf("只有一个参数\n");
    }
    return 0;
}
L-96FCG8WP-1504:untitled renxiaohui$ gcc text30.c 
L-96FCG8WP-1504:untitled renxiaohui$ a.out text
参数为 text

参考

www.runoob.com/cprogrammin…