在程序的世界里,我们往往在做一些分而治之的事情。

一开始我们写的所有程序都在main()里面,然后写着写着呢我们会觉得main()太大了,于是我们会分出一些函数出来,所以我们有了函数,把一个又一个功能从main()中剥离出来放在函数里面。

后来我们会发现一个.c文件里函数越来越多,于是我们开始把函数从一个.c文件里拿出来放到很多个.c文件中去,可是当我们把函数从一个.c文件里拿出来放到很多个.c文件中去之后,又该怎么组合成一个有效的程序呢?

从编译器的角度来看,一个.c文件是一个编译单元,编译器每次编译只处理一个编译单元,编译完之后形成.o文件(目标代码文件),然后由链接器去链接起来。

头文件

把函数原型放到一个头文件(以.h结尾)中,在需要调用这个函数的源代码文件(.c文件)中#include这个头文件,就能让编译器在编译的时候知道函数的原型,否则程序有可能编译成功,但参数类型是由编译器推测出来的,导致出现异常的情况。

#include

  • #include是一个编译预处理指令,和宏一样,在编译之前就处理了
  • 它把那个文件的全部文本内容原封不动地插入到它所在的地方
  • 所以也不是一定要在.c文件的最前面用#include

“”还是<>

  • #include有两种形式来指出要插入的文件
  • “”要求编译器首先在当前目录(.c文件所在的目录)寻找这个文件,如果没有,到编译器指定的目录去找
  • <>让编译器只在指定的目录去找
  • 编译器知道自己的标准库头文件在哪里
  • 环境变量和编译器命令行参数也可以指定寻找头文件的目录

#include的误区

  • #include不是用来引入库的
  • stdio.h里只有printf()的原型,printf()的代码在另外的地方,某个.lib(Windows)或.a(Unix)中
  • 现在的C语言编译器默认会引入所有的标准库
  • #include<stdio.h>只是为了让编译器知道printf()函数的原型,保证你调用时给出的参数值是正确的类型

不对外公开的函数

  • 在函数前面加上static就使得它成为只能在所在的编译单元中(当前.c文件中)被使用的函数
  • 在全局变量前面加上static就使得它成为只能在所有的编译单元中被使用的全局变量

变量的声明

  • int i;//变量的定义
  • extern int i;//变量的声明

声明和定义

  • 声明是不产生代码的东西
  • 定义是产生代码的东西

标准头文件结构

  • 运用条件编译和宏,保证这个头文件在一个编译单元中只会被#include一次
  • #pragma once也能起到相同的作用,但是不是所有的编译器都支持

编译预处理指令

  • #开头的是编译预处理指令
  • 它们不是C语言的成分,但是C语言离不开它们
  • #define用来定义一个宏
  • #include用来包含一个头文件

C语言程序在编译之前会进行编译预处理,在编译预处理过程中会把所有的#define定义的宏进行替换。

C语言编译过程中会产生一些临时文件,在GCC编译器编译过程中如下:

main.c->main.i->main.s->main.o->a.out

  1. 由源代码main.c进行编译预处理得到main.i
  2. 由编译预处理后的代码文件main.i进行编译得到汇编代码文件main.s
  3. 汇编代码文件main.s做汇编得到目标代码文件main.o
  4. 目标代码文件main.o进行链接形成可执行文件a.out

#define

  • #define <名字><值>
  • 注意没有结尾的分号,因为不是C的语句
  • 名字必须是一个单词,值可以是各种东西
  • 在C语言的编译器开始编译之前,编译预处理程序会把程序中的名字换成对应的值,仅仅是做的完全的文本替换
  • 使用gcc –save-temps可以保存编译过程中的临时文件

  • 如果一个宏的值中有其他的宏的名字,也是会被替换的
  • 如果一个宏的值超过一行,最后一行之前的行末要加\
  • 宏的值后面出现的注释不会被当作宏的值的一部分

没有值的宏

  • #define _DEBUG
  • 这类宏是用于条件编译的,后面有其他的编译预处理指令来检查这个宏是否已经被定义过了
  • 在金山WPS实习开发WPS for Mac的时候遇到过,只有在Mac环境下编译才执行的一段代码

预定义的宏

  • __func__ //函数的函数名
  • __LINE__ //源代码文件的行号
  • __FILE__ //源代码文件的文件名
  • __DATE__ //编译时的日期
  • __TIME__ //编译时的时间
  • __STDC__ //判断该文件是不是标准C程序,当要求程序严格遵循ANSIC标准时该标识符被赋值为1
#include<stdio.h>

int dxf(void);

int main(int argc,int *argv[])
{
    dxf();
    return 0;
}

int dxf(void)
{
    printf("%s:%d\n",__FILE__,__LINE__);//输出源代码文件名和目前的行号
    printf("%s\n",__func__);//输出函数名
    printf("%s,%s\n",__DATE__,__TIME__);//输出编译的日期和时间
}

 

运行结果如下:

带参数的宏和像函数的宏

  • #define cube(x) ((x)*(x)*(x))
  • 宏可以带参数

由于预编译过程中#define仅仅是简单的文本替换,所以容易出现运算优先级问题,因此在定义带参数的宏的时候应该遵循一些原则

带参数的宏的原则

  • 一切都要括号(整个值要括号,参数出现的每个地方都要括号)
  • #define RADTODEG(x) ((x)*57.29578)

带参数的宏

  • 可以带多个参数,如#define MIN(a,b) ((a)>(b)?(b):(a))
  • 也可以组合嵌套使用其他宏
  • 在大型程序的代码中使用非常普遍
  • 部分宏会被inline函数替代

宏的缺点

  • 宏的参数没有类型检查,处理不了特殊的输入,而内联函数inline的引入正是为了解决这个问题

宏展开的灵活运用

在Arduino的Ethernet库的w5100.cpp里有这样的函数调用:

writeTMSR(0x55);

但是遍寻整个.cpp和对应的w5100.h也找不到这个writeTMSR()函数,即使把所有的源代码目录拿来搜索一遍都没有。但是,编译显然是通过了的,那么,这个函数在哪里呢?

在w5100.h,我们发现了这样的代码:

#define __GP_REGISTER8(name, address)             \
  static inline void write##name(uint8_t _data) { \
    write(address, _data);                        \
  }                                               \
  static inline uint8_t read##name() {            \
    return read(address);                         \
  }

于是,在w5100.h里接下去的代码:

__GP_REGISTER8 (TMSR,   0x001B);    // Transmit memory size

在编译预处理后,就会被展开成为:

static inline void writeTMSR(uint8_t _data) {
    write(0x001B, _data);            
}
static inline uint8_t readTMSR() {
    return read(0x001B);             
}

其中##是一种分隔连接方式,它的作用是先分隔,然后进行强制连接。

全局变量

  • 定义在函数外面的变量是全局变量
  • 全局变量具有全局的生存期和作用域,它们与任何函数都无关,在任何函数内部都可以使用它们

例子:

#include<stdio.h>

int f(void);
int gAll = 12;

int main(int argc,int *argv[])
{
    printf("in %s gAll=%d\n",__func__,gAll);
    f();
    printf("in %s gAll=%d\n",__func__,gAll);
    return 0;
}

int f(void)
{
    printf("in %s gAll=%d\n",__func__,gAll);
    gAll = gAll + 2;
    printf("in %s gAll=%d\n",__func__,gAll);
    return gAll;
}

其中__func__为当前函数的名字。

该代码的输出

说明全局变量和任何函数都没有关系。

全局变量初始化

  • 没有做初始化的全局变量会得到0值
  • 没有做初始化的指针会得到NULL值
  • 只能用编译时刻已知的值来初始化全局变量
  • 它们的初始化发生在main函数之前

被隐藏的全局变量

  • 如果函数内部存在与全局变量同名的变量,则全局变量被隐藏

静态本地变量

  • 在本地变量定义时加上static修饰符就成为静态本地变量
  • 当函数离开的时候,静态本地变量会继续存在并保持其值(生存期全局,作用域本地)
  • 静态本地变量的初始化只会在第一次进入这个函数时做,以后进入函数时会保持上次离开时的值
  • 静态本地变量实际上是特殊的全局变量,它们位于相同的内存区域

例子:

#include<stdio.h>

int f(void);

int main(int argc,int *argv[])
{
    f();
    f();
    f();
    return 0;
}

int f(void)
{
    static int all = 1;
    printf("in %s all=%d\n",__func__,all);
    all = all + 2;
    printf("in %s all=%d\n",__func__,all);
    return all;
}

 

运行结果如下:

 

返回指针的函数

  • 返回本地变量的地址是危险的
  • 返回全局变量或静态本地变量的地址是安全的
  • 返回在函数内malloc的内存是安全的,但是容易造成问题
  • 最好的做法是返回传入的指针

Tips

  • 不要使用全局变量来在函数间传递参数和结果
  • 尽量避免使用全局变量
  • 使用全局变量和静态本地变量的函数是线程不安全的

可变数组实现

怎么样实现一个大小可变的数组?(只考虑输入正常的情况,暂不考虑越界等问题)

  • 数组可以增长
  • 能够方便的得到数组的大小
  • 能够访问数组中的单元

需要实现的函数:

  • Array array_create(int init_size);//创建一个初始大小为init_size的int类型数组
  • void array_free(Array *a);//释放数组
  • int array_size(const Array *a);//获得数组此时的大小
  • int array_get(const Array *a , int index);//访问数组的第index个单元
  • void array_set(Array *a , int index , int value);//设置数组的第index个单元
  • void array_inflate(Array *a,int more_size);//给数组增加more_size个单元

实现代码如下:

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

 typedef struct{
      int *array;
      int size;
 }Array;

Array array_create(int init_size);//创建一个初始大小为init_size的int类型数组
void array_free(Array *a);//释放数组
int array_size(const Array *a);//获得数组此时的大小
int array_get(const Array *a , int index);//访问数组的第index个单元
void array_set(Array *a , int index , int value);//设置数组的第index个单元
void array_inflate(Array *a,int more_size);//给数组增加more_size个单元

Array array_create(int init_size)//创建一个初始大小为init_size的int类型数组
{
    Array a;
    a.size = init_size;
    a.array = (int*)malloc(a.size*sizeof(int));
    return a;
}

void array_free(Array *a)//释放数组
{
     free(a->array);
     a->array = NULL;
     a->size = 0;
}

int array_size(const Array *a)//获得数组此时的大小
{
     return a->size;
}

int array_get(Array *a,int index)//访问数组的第index个单元
{
     return a->array[index];

}

void array_set(Array *a,int index ,int value)//设置数组的第index个单元
{
     if(index >= a->size){
        array_inflate(a,(index/20+1)*20 - a->size);
     }
      a->array[index] = value;
}

void array_inflate(Array *a,int more_size)//给数组增加more_size个单元
{
    int *p = (int*)malloc((a->size+more_size)*sizeof(int));
     for(int i = 0; i<a->size ;i++){
        p[i] = a->array[i];
     }
    free(a->array);
    a->size =a->size+more_size;
    a->array = p;
}

 int main(){
      Array a = array_create(1);
      array_set(&a,0,1);
      printf("array_get(&a,0)=%d\n",array_get(&a,0));
      printf("array_size(&a)=%d\n",array_size(&a));
      array_set(&a,1,10);
      printf("array_size(&a)=%d\n",array_size(&a));
      printf("array_get(&a,0)=%d\n",array_get(&a,0));
      printf("array_get(&a,1)=%d\n",array_get(&a,1));
 }

 

题目描述:

n条恶龙闯入了王国的领土,为了拯救王国的危机,国王任命你前往冒险者工会雇佣勇者前去讨伐恶龙。每条恶龙的战斗力为ai。每个勇者的战斗力为bi,雇佣他的花费为ci。只有当勇者的战斗力大于等于恶龙的战斗力时,勇者才能击败恶龙。因为勇者都是骄傲的,所以勇者们拒绝互相组队(即每个勇者只能独自一人去讨伐恶龙)。勇者们都具有凝聚空气中魔力的能力,因此可以无限次出战。王国需要金币去灾后重建,需要你计算打败所有恶龙的最小花费。

输入:

第一行输入一个整数T,表示数据的组数。1≤T≤10。 第一行输入n,m,表示龙的数量和冒险者的数量。0<n,m≤10000。 接下来n行,每行一个数字ai表示龙的战斗力。 接下来m行,每行分别为bi和ci,表示冒险者的战斗力和雇佣他的花费。0<=ai,bi,ci≤100000。

输出:

如果能击退恶龙们,请输出最小的花费,如果不能,请输出”Kingdom fall”

样例输入:

2

1 1

6

10 3

3 2

10

14

15

9 10

6 7

样例输出:

3 Kingdom fall

 

先对每个骑士和龙的能力进行sort排序,然后遍历骑士和龙,用最小的代价将龙一条一条击退就好啦~
AC代码:

#include<iostream>
#include<limits.h>
#include<algorithm>
using namespace std;

struct maoxianjia
{
    double att;
    double mesos;
};

int comp(const maoxianjia &a,const maoxianjia &b)
{
    if(a.att == b.att)
    {
        return a.mesos<b.mesos;
    }
    else
        return a.att>b.att;
}

int main()
{
    int t = 0;
    int n = 0;
    int m = 0;
    int sum = 0;
    int mini;
    cin>>t;
    while(t--)
    {
        sum = 0;
        cin>>n>>m;
        int a[n+1];
        for(int i = 0; i < n; i++)
        {
            cin>>a[i];
        }
        maoxianjia b[m+1];
        for(int i = 0; i < m ; i++)
        {
            cin>>b[i].att>>b[i].mesos;
        }
        sort(a,a+n);
        sort(b,b+m,comp);
        if(b[0].att<a[n-1])
        {
            cout<<"Kingdom fall"<<endl;
            continue;
        }
        else
        {
            mini = INT_MAX;
            for(int i = 0; i <n ; i++)
            {
                for(int j = 0; j<m ; j++)
                {
                    if(a[i] > b[j].att)
                    {
                        break;
                    }
                    if(mini > b[j].mesos)
                    {
                        mini = b[j].mesos;
                    }
                }
                sum = sum + mini;
                mini = INT_MAX;
            }
        }
        cout<<sum<<endl;
    }
}

ACLLib是一个纯教学用途的纯C语言图形库,它并非任何产业界在使用的图形库,也不会有机会发展成为流行的图形库。它只是我们为了C语言学习的目的用的非常简单的图形库。它基于MS Windows的Win32API,所以在所有的Windows版本上都能使用。但是也因此它无法做成跨平台的库在其他操作系统上使用。

ACLLib在github上开源,网址是:https://github.com/wengkai/ACLLib

ACLLib是一个基于Win32API的函数库,提供了相对较为简单的方式来做Windows程序。

Windows API

  • 从第一个32位的Windows开始就出现了,就叫做Win32API
  • 它是一个纯C的函数库,就和C标准库一样,使你可以写Windows应用程序
  • 过去很多Windows程序是使用这个API做出来的

main()

  • main()成为C语言的入口函数其实和C语言本身无关,你的代码是被一小段叫做启动代码的程序所调用的,它需要一个叫做main的地方
  • 操作系统把你的可执行程序装载到内存里,启动运行,然后调用你的main函数
  • Win32API启动代码寻找的入口函数就是WinMain()而不是main()

CodeBlocks使用ACLLib图形库配置方法:

  1. 去https://github.com/wengkai/ACLLib下载src目录下的两个主要文件acllib.c和acllib.h
  2. 打开CodeBlocks,File->New->Project,创建一个新的项目,选择空项目即可
  3. 在项目的目录下新建一个main.c文件,并将acllib.c和acllib.h复制到该项目目录下,通过CodeBlocks的Project->Add file将这两个文件都添加到项目里来
  4. 通过CodeBlocks的Setting->Compiler,选择Linker settings,在CodeBlocks安装目录下的MinGW文件夹中的lib文件夹中找到下列8个文件add进去,点击OK即可。
  5. 然后打开main.c,即可开始写我们的代码啦~

我们ACLLib的程序的基本框架如下:(main.c)

#include "acllib.h"

int Setup(){
    initWindow("test",DEFAULT,DEFAULT,width,width);
    return 0;
}

我们的入口函数是大写开头的Setup(),在里面可以使用initWindow()去初始化一个窗口。

void initWindow(const char title[] , int left , int top , int width , int height);

参数含义依次为:窗口的标题,窗口的左边起始坐标,上面起始坐标,窗口宽度,窗口高度。

在窗口里面画图的代码要放在beninPaint()和endPaint()之间,代表画图的开始和画图的结束,任何绘图函数的调用必须在这一对函数调用之间才会生效。

在Windows中,坐标是以像素点的数字来定义的。对于你创建出来的窗口,左上角是(0,0),x轴自左向右增长,而y轴自上向下增长。

在终端窗口中,如果需要使用scanf()和printf()进行输入输出,则首先要使用initConsole()初始化,并且要记得加上stdio.h这个头文件,然后就可以在那个终端窗口使用scanf()和printf()了。

  • void putPixel(int x,int y,ACL_Color color);//参数列表依次为x坐标,y坐标,颜色
  • ACL_Color getPixel(int x, int y);

颜色

  • RGB(r,g,b)
  • 红色->RGB(255,0,0)
  • 图形库预先定义了一些颜色可以直接使用,如BLACK,RED,GREEN,BLUE,CYAN,YELLOW,WHITE,MAGENTA

线

  • void moveTo(int x, int y);//移动当前坐标到(x,y)
  • void moveRel(int dx , int dy);//将当前位置移动dx和dy的相对距离
  • void line(int x0,int y0,int x1,int y1);//画一条从(x0,y0)到(x1,y1)的直线
  • void lineTo(int x,int y);//从当前点画线到(x,y)
  • void lineRel(int dx,int dy);//从当前位置点到与当前点相对距离为dx和dy的点画一直线
  • void arc(int nLeftRect,int nTopRect,int nRightRect,int nBottomRect,int nXStartArc,int nYStartArc,int nXEndArc,int nYEndArc);//画一条圆弧,参数列表依次为完整圆弧外接矩形的左坐标,顶部坐标,右坐标,底部坐标,圆弧开始的x坐标,y坐标,结束的x坐标,y坐标

画笔

  • void setPenColor(ACL_Color color);//给画笔设置颜色
  • void setPenWidth(int width);//设置画笔的宽度,单位为像素点
  • void setPenStyle(ACL_Pen_Style style);//设置画笔的风格
  • PEN_STYLE_SOLID//实线———-
  • PEN_STYLE_DASH//虚线 – – – – –
  • PEN_STYLE_DOT//点线 ··········
  • PEN_STYLE_DASHDOT//虚线加点线-·-·-·-·-·-·
  • PEN_STYLE_DASHDOTDOT//虚线加两个点的线-··-··-··-··
  • PEN_STYLE_NULL//画一条看不见的线

  • void chrod(int nLeftRect , int nTopRect , int nRightRect , int nBottomRect, int nXRadia1l,int nYRadial1,int nXRadial2,int nYRadial2);//画扇形
  • void ellipse(int nLeftRect , int nTopRect,int nRightRect,int nBottomRect);//画椭圆
  • void pie(int nLeftRect , int nTopRect , int nRightRect , int nBottomRect, int nXRadia1l,int nYRadial1,int nXRadial2,int nYRadial2);//画饼
  • void rectangle(int nLeftRect , int nTopRect,int nRightRect, int nBottomRect);//画矩形
  • void roundrect(int nLeftRect,int nTopRect,int nRightRect,int nBottomRect,int nWidth,int nHeight);//画圆角矩形

刷子

  • 画笔负责线及面的边缘,刷子负责面的内部
  • void setBrushColor(ACL_Color color);//设置刷子的颜色
  • void setBrushStyle(ACL_Brush_Style style);//设置刷子的风格
  • BRUSH_STYLE_SOLID
  • BRUSH_STYLE_HORIZONTAL
  • BRUSH_STYLE_VECRTICAL
  • BRUSH_STYLE_FDIAGONAL
  • BRUSH_STYLE_BDIAGONAL
  • BRUSH_STYLE_CROSS
  • BRUSH_STYLE_DIAGCROSS

文字

  • void setTextColor(ACL_Color color);//设置文字的颜色
  • void setTextBkColor(ACL_Color color);//设置文字的背景颜色
  • void setTextSize(int size);//设置文字的字体大小
  • void setTextFont(char *pFontName);//设置文字的字体风格
  • void paintText(int x,y,const char *pStr);//以坐标点(x,y)为左上角写文字,文字内容为字符串pStr

练手画了一个Microsoft的Logo。

#include "acllib.h"

int Setup(){
    initConsole();

    int width = 400;
    initWindow("Microsoft",DEFAULT,DEFAULT,width,width);
    beginPaint();

    setPenColor(RGB(246,83,20));
    setBrushColor(RGB(246,83,20));
    rectangle(0,0,200,200);//左上角

    setPenColor(RGB(124,187,0));
    setBrushColor(RGB(124,187,0));
    rectangle(200,0,400,200);//右上角

    setPenColor(RGB(0,161,241));
    setBrushColor(RGB(0,161,241));
    rectangle(0,200,200,400);//左下角

    setPenColor(RGB(255,187,0));
    setBrushColor(RGB(255,187,0));
    rectangle(200,200,400,400);//右下角

    endPaint();
    return 0;
}