在C++中,类对象的存储空间分配方式如下:

(1)一个类对象的分配空间中仅包含所有非静态数据成员,并按照这些数据成员定义的顺序存放。

(2)一个类对象的大小为其所有非静态数据成员的类型大小之和,普通成员函数与sizeof是没有关系的。当类中有一个或者多个虚函数时,由于要维护虚函数表,所以有一个虚函数表指针,另外当存在虚继承时还需要一个指向虚基类的指针,每个指针占用一个地址大小的内存空间,即4个字节。

(3)类对象中的各个数据成员分配内存时也遵守内存对齐规则。内存对齐规则请看:内存对齐

在了解了对象的存储结构后,可以采用取指定地址中值的方式访问对象的私有数据成员,如下程序所示:

#include<iostream>
using namespace std;

class A{
private:
    int x;
    int y;
public:
    A(int i,int j){
        x = i;
        y = j;
    }
    
};

int main()
{
    A a(1,3);
    cout<<"a.x="<<*((int *)(&a))<<endl;
    cout<<"a.y="<<*((int *)(&a)+1)<<endl;
}

输出为:

a.x=1
a.y=3
Program ended with exit code: 0

 

assert宏(在assert.h头文件中定义)用于测试表达式的值,如果表达式的值为假,则assert输出错误信息,并调用函数abort()以结束程序。这是测试某个变量是否具有正确值的有用的调试工具例如以下语句就是一个断言:

assert(x<10);

当在程序中遇到这个语句时,如果x的值大于或等于10,则将打印包含行号和文件名的错误信息,而且程序终止。然后程序将在这个代码区域内查找错误。

void  *memcpy(void *&pvTo, const void *pvFrom, size_t size) {
       char *pbTo = (char*)pvTo;
       char *pbFrom = (char*)pvFrom;
       assert((pvTo != NULL) && (pvFrom != NULL));      
//使用断言检查输入指针的有效性
       assert(pbTo >= pbFrom + size ||  pbFrom >= pbTo + size);
//使用断言检查两个指针指向的内存区域是否重叠
       while(size -- > 0 )
              *pbTo++ = *pbFrom++ ;
}

可以通过定义 NDEBUG 来关闭 assert,但是需要在源代码的开头,include <assert.h> 之前。

 

设计一个日期类,要求重载其输入输出运算符,方便输入输出(输入和输出的格式为1970.09.25),并要求实现日期加和减运算(即(1)一个日期减另一个日期返回值为这两个日期之间相隔的天数;(2)一个日期减一个整数8则返回比这个日期早8天的日期;(3)一个日期加一个整数8则返回比这个日期晚8天的日期),并能比较两个日期的先后顺序,在main函数中首先要求用户输入两个日期,然后首先输出第一个日期加100天和减100天是哪一天?再输出第一个日期与第二个日期之间相隔多少天?最后将两个日期按先后次序输出。(考虑闰年,考虑月份不同)

#include<iostream>
#include<algorithm>
#include <ctime>
using namespace std;

class Date
{
private:
    int year;
    int month;
    int day;

public:
    Date()
    {
        year = 0;
        month = 0;
        day = 0;
    }

    Date(int a,int b,int c)
    {
        year = a;
        month = b;
        day = c;
    }

    ~Date()
    {

    }

    int getYear()
    {
        return this->year;
    }

    void setYear(int year)
    {
        this->year = year;
    }

    int getMonth()
    {
        return this->month;
    }

    void setMonth(int month)
    {
        this->month = month;
    }

    int getDay(int day)
    {
        return this->day;
    }

    void setDay()
    {
        this->day =day;
    }
    bool IsLeapYear(int year)
    {
        if(year % 400 || (year % 4 && year % 100))
            return true;
        return false;
    }
    int YearsOfMonth(int year, int month)
    {
        int day;
        int days[13] = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
        day = days[month];
        if (month == 2)
            day += IsLeapYear(year);
        return day;
    }

    Date operator+(int a)
    {
        Date b;
        b.year = this->year;
        b.month = this->month;
        b.day = this->day + a;
        while(b.day > YearsOfMonth(b.year,b.month))
        {
            if(b.month == 12)
            {
                b.month = 1;
                b.year++;
            }
            else
            {
                b.month++;
            }
            b.day = b.day - YearsOfMonth(b.year,b.month);
        }
        return b;
    }

    Date operator-(int a)
    {
        Date b;
        b.year = this->year;
        b.month = this->month;
        b.day = this->day - a;
        while(b.day <= 0)
        {
            if(b.month == 1)
            {
                b.month = 12;
                b.year--;
            }
            else
            {
                b.month--;
            }
            b.day = b.day + YearsOfMonth(b.year,b.month);
        }
        return b;
    }

    int daysOfDate(Date d)//计算一共的天数
    {
        int days=d.day;
        for(int y=1; y<d.year; y++) //计算年
            days= days + 365+IsLeapYear(d.year);
        for(int m=1; m<d.month; m++) //计算月
            days= days + YearsOfMonth(d.year,d.month);
        return days;
    }

    int operator-(Date a)
    {
        Date b;
        b.year = this->year;
        b.month = this->month;
        b.day = this->day ;
        int days1=daysOfDate(b);
        int days2=daysOfDate(a);
        return days1 - days2;
    }

    bool operator>(Date a)
    {
        Date b;
        b.year = this->year;
        b.month = this->month;
        b.day = this->day ;
        int days1=daysOfDate(b);
        int days2=daysOfDate(a);
        if(days1 >= days2)
        {
            return true;
        }
        return false;
    }

    friend    ostream& operator<<(ostream& out, const Date &a)
    {
        out<<a.year<<"."<<a.month<<"."<<a.day;
        return out;
    }


    friend    istream& operator>>(istream& in, Date &a)
    {
        in >>a.year;
        cin.get();
        in>>a.month;
        cin.get();
        in>>a.day;
        return in;
    }
};

int main()
{
    Date a;
    Date b;
    cin>>a;
    cin>>b;
    cout<<a+100<<endl;
    cout<<a-100<<endl;
    cout<<a-b<<endl;
    if(a>b)
    {
        cout<<b<<endl;
        cout<<a<<endl;
    }
    else
    {
        cout<<a<<endl;
        cout<<b<<endl;
    }
}

 

五子棋是一种两人对弈或者人机对弈的纯策略型棋类游戏,应用C语言编写程序可以在计算机上实现两人对弈和人机对弈五子棋功能。人机对弈五子棋程序由开始界面,棋盘,判断胜负和AI等子函数构成;程序中应用了数组、全局变量、图形编程等元素和语句。程序通过棋盘和棋子图像生成、玩家移子与电脑判断分数高低而落子和判断胜负等功能的实现,在Windows操作系统上用CodeBlocks实现了两人对弈和人机对弈模式。

源代码已传github:MapleStory GoBang

第一章    序 言

1.1 设计背景

五子棋相传起源于四千多年前的尧帝时期,比围棋的历史还要悠久,可能早在“尧造围棋”之前,民间就已有五子棋游戏。有关早期五子棋的文史资料与围棋有相似之处,因为古代五子棋的棋具与围棋是完全相同的。在上古的神话传说中有“女娲造人,伏羲做棋”一说,《增山海经》中记载:“休舆之山有石焉,名曰帝台之棋,五色而文状鹑卵。”李善注引三国魏邯郸淳《艺经》中曰:“棋局,纵横各十七道,合二百八十九道,白黑棋子,各一百五十枚”。这段虽没明讲是何种棋类,但至少知道远古就以漂亮的石头为棋子。因而规则简单的五子棋也可能出自当时,并是用石子作棋子。亦有传说,五子棋最初流行于少数民族地区,以后渐渐演变成围棋并在炎黄子孙后代中遍及开来。在古代,五子棋棋具虽然与围棋相类同,但是下法却是完全不同的。正如《辞海》中所言,五子棋是“棋类游戏,棋具与围棋相同,两人对局,轮流下子,先将五子连成一行者为胜”。

传统五子棋的棋具与围棋相同,棋子分为黑白两色,棋盘为15×15,棋子放置于棋盘线交叉点上。 因为传统五子棋只能实现人人对战,而用计算机编程能够实现人机对战,一个人的时候也能体验五子棋的乐趣。因此我们设计了一款拥有双人对战和人机对战的五子棋游戏。

1.2 设计目的

五子棋游戏不仅能增强人们的抽象思维能力、逻辑推理能力、空间想象力,提高人们的记忆力、心算能力等,而且深含哲理,有助于修身养性。五子棋既有现代休闲方式所特有的特征“短、平、快”,又有中国古典哲学所包含的高深学问“阴阳易理”。它既有简单易学的特点,为人民群众所喜闻乐见,又有深奥的技巧;既能组织举办群众性的比赛、活动,又能组织举办高水平的国际性比赛;它的棋文化源渊流长,具有东方的神秘和西方的直观,它是中西方文化的交融点,也是中西方文化交流的一个平台。

自从计算机作为游戏对战平台以来,各种棋类游戏如雨后春笋般纷纷冒出。五子棋是一种受大众广泛喜爱的游戏,其规则简单,变化多端,非常富有趣味性和消遣性。同时具有简单易学、既动手又动脑的特点。同时也为锻炼自己的编程能力。

第二章 需求分析

根据功能需求,将程序分为图形显示、玩家电脑走棋,胜负判断和AI判断落子位置四个函数,以下分析各模块的需求。

2.1 图形库

图形库我们使用的是ACLLib,ACLLib是一个纯教学用途的纯C语言图形库,它并非任何产业界在使用的图形库,也不会有机会发展成为流行的图形库。它只是我们为了C语言学习的目的用的非常简单的图形库。它基于MS Windows的Win32API,所以在所有的Windows版本上都能使用。但是也因此它无法做成跨平台的库在其他操作系统上使用。程序开始运行时,给出五子棋游戏名称界面,让玩家选择双人模式或者人机模式,并选择哪一方先落子。游戏开始后要求生成15×15的棋盘图像,游戏进行过程中,要求实时显示棋盘上已落下的棋子,分出胜负后,在棋盘上方显示哪一方获胜的信息。

2.2 玩家控制模块

程序开始时,需要选择游戏模式,是否让电脑先落子,才能开始游戏;游戏过程中,玩家通过鼠标点击棋盘选择落子;游戏结束时,会显示哪一方获得了胜利。

2.3 胜负判断模块

每当棋盘上多下了一颗棋子,就检测棋盘上新下的这颗棋子,一旦出现五子连线,终止游戏程序,在棋盘上方显示玩家输赢语句。

2.4电脑计分判断落子位置模块

在能下棋的空位置中,假设电脑和玩家下在此处,分别算出各个空位置的分数,并找出最大值。如果最大值是玩家下的,那么电脑就“抢”他的位置,即做到了“防守”。如果最大值是电脑下的,那就让电脑在此处下,即做到了“进攻”,如果存在多个最大值,那么简单的调用随机函数选择其中一个。

第三章 程序详细设计

3.1 开发环境

开发环境:

操作系统:Windows 10 64位

IDE:CodeBlocks 64位

编译器:GNU GCC Compiler

图形库:ACLLib

3.2 ACLLib图形库介绍及开发环境配置

详情请看ACLLib图形库初试

3.3 AI设计思路

五子棋的棋盘是15*15的大小。我的AI算法要求每一次落子之后都要去计算每一个空暇的位置的“分值”,简单的说,我们需要一个存放棋子的数组,表示是否存放了棋子,还要一个计算每一个空格的数组来记录“分数”,这个分数是后期AI用来运算的基础,也是你AI难度控制的点。

我现有的思路就是分两部分。首先是如果是玩家先落子,那么要求电脑AI随即在你落子的地方的任意一个方向,随机落子,这是第一步。接下来以后就正式进入到算法中去。

首先初始化你的分数数组,让他们全部为零。然后在每一次落子之后进行全盘的遍历,如果发现该处为空白,于是检查其四周八个方向,当然如果是边缘位置就相对修改,判断是否出了边界。若在空白处,且发现在某一对角线方向发现有一个其他颜色的棋子,那么相对的给这个空白区域的分数数组加上一定的分值,然后继续往这个方向检测是否还有连续的同一颜色的棋子,若没有则检查其他方向或者检测下一个空白位置。若是还在同一方向上面找到了相同颜色的棋子,那么第二个棋子的出现,你可以给改空白处加上双倍的分值,表明这个空白位置更加重要。依次类推,继续检测。最终AI棋子落在什么地方,依靠的是最后遍历整个分数数组,然后根据分数的高低来进行判断落子落在哪里的。

经过上一遍的遍历,每一次落子都会使得分数数组得到一些变化,每一次都会导致AI判断的变化。在这个基础上,每一次落子还要进行一次对自己本身棋子颜色的一个遍历,判断自己的情况,同时加分加在分数数组之中,这样一来,电脑就会根据自己的棋子的情况以及玩家的落子情况进行判断,哪一个地方更加适合落子。

3.4 函数说明

int isWin(int row, int col,int whoFirst);

判断当棋子下在棋盘上的(row,col)时是否分出胜负。

int Setup();

加载游戏首页的一些图片资源。

void chooseMode(void);

选择游戏的模式,有人机模式和双人模式两种。

void showIndex(void);

显示游戏首页,选择棋子种类。

void initImage1(void);

加载游戏中的一些游戏资源。

void playMusic(void);

加载游戏的背景音乐。

void mouseListener0(int x , int y ,int button ,int event);

监听鼠标,在首页时当鼠标移至指定位置时该位置的文字显示高亮,以及鼠标的点击位置。

void mouseListener1(int x , int y ,int button ,int event);

监听鼠标,在选择游戏模式界面时当鼠标移至指定位置时该位置的文字显示高亮,以及鼠标的点击位置。

void mouseListener2(int x , int y ,int button ,int event);

监听鼠标,判断人机模式下玩家的落子坐标。

void mouseListener3(int x , int y ,int button ,int event);

监听鼠标,判断双人模式下玩家的落子坐标。

char* myStrCat(char *dst,const char *src);

自己写的一个字符串处理函数,处理资源路径用。

void gameMode0(void);

人机模式。

void gameMode1(void);

双人模式。

void timerListener(int id);

定时器,用于给上一个落子地点产生闪烁提示玩家。

char * myStr(char *str,int n,int m);

自己写的一个字符串处理函数,处理资源路径用。

void Robot(int *x, int *y, int *Sum);

人机模式判断落子。

void Findscore(int *x, int *y);

查找评分最高的坐标。

void ChessOne(int *x, int *y);

玩家走第1步时的落子。

void ChessScore();

给当前棋盘没有棋子的地方打分。

 

第四章 游戏运行展示

在 C 语言中,我们不能使用 goto 语句来跳转到另一个函数中,但提供了两个函数——setjmp 和 longjmp来完成这种类型的分支跳转。

我们都知道要想在一个函数内进行跳转,可以使用 goto 语句(不知怎么该语句在中国学生眼中就是臭名昭著,几乎所有国内教材都一刀切地教大家尽量不要使用它,但在我看来,这根本不是语言的问题,而是使用该语言的人,看看 Linux 内核中遍地是 goto 语句的应用吧!),但如果从一个函数内跳转到另一个函数的某处,goto 是不能完成的,那该如何实现呢?

函数原型

#include <setjmp.h>
int setjmp(jmp_buf env);

setjmp 函数的功能是将函数在此处的上下文保存在 jmp_buf 结构体中,以供 longjmp 从此结构体中恢复。

  • 参数 env 即为保存上下文的 jmp_buf 结构体变量;
  • 如果直接调用该函数,返回值为 0; 若该函数从 longjmp 调用返回,返回值为非零,由 longjmp 函数提供。根据函数的返回值,我们就可以知道 setjmp 函数调用是第一次直接调用,还是由其它地方跳转过来的。
void longjmp(jmp_buf env, int val);

longjmp 函数的功能是从 jmp_buf 结构体中恢复由 setjmp 函数保存的上下文,该函数不返回,而是从 setjmp 函数中返回。

  • 参数 env 是由 setjmp 函数保存过的上下文。
  • 参数 val 表示从 longjmp 函数传递给 setjmp 函数的返回值,如果 val 值为0, setjmp 将会返回1,否则返回 val。
  • longjmp 不直接返回,而是从 setjmp 函数中返回,longjmp 执行完之后,程序就像刚从 setjmp 函数返回一样。
#include <stdio.h>    
#include <setjmp.h>    
#include <windows.h>  
   
jmp_buf jmpbuffer;  
int i = 0;  
void test_jmp()  
{  
    ++i;  
    longjmp(jmpbuffer, i);  //跳转到setjmp处  
}  
   
int main(int argc, char **argv)  
{  
    int  ret = 0;  
    if ((ret = setjmp(jmpbuffer)) != 0) //类似于goto所用的tag,告诉longjmp应该返回到哪里    
    {  
        printf("jmp:%d\n", ret);  
        Sleep(200);  
    }  
    test_jmp();  
    return 0;  
}

 

按位运算

C语言有这些按位运算的运算符

  • &   按位的与
  • |    按位的或
  • ~  按位取反
  • ^   按位的异或
  • << 左移
  • >> 右移

按位与&

  • 如果x的第i位是1且y的第i位是1,那么(x&y)的第i位是1,否则的话(x&y)的第i位是0
  • 按位与常用于两种应用:
  • 让某一位或某些位为0
  • 取一个数中的一段

按位或|

  • 如果x的第i位是1或y的第i位是1,那么(x|y)的第i位是1,否则的话(x|y)的第i位是0
  • 按位或常用于两种应用:
  • 让某一位或某些位为1
  • 把两个数拼起来

按位取反~

  • 把1位变0,0位变1
  • 想要得到全部位为1的数:~0

逻辑运算VS按位运算

  • 对于逻辑运算,它只看到两个值:0和1
  • 可以认为逻辑运算相当于把所有非0值都变成1,然后做按位运算

按位异或

  • 如果x的第i位和y的第i位相等,那么(x^y)的第i位是0,否则的话(x^y)的第i位是1
  • 如果两个位相等,那么结果为0;不相等,结果为1
  • 对一个变量用同一个值异或两次,等于什么也没做

曾经面试金山WPS的时候有一道题问到:不使用额外的空间,交换两个整形数字。

有两种方法

方法一:算术方法

x = x + y;
y = x - y;
x = x - y;

方法二:异或方法

x = x^y;// 只能对int,char..
y = x^y;
x = x^y;

移位运算:左移<<

  • i << j
  • i中所有的位向左移动j个位置,而右边填入0
  • 所有小于int的类型,移位以int的方式来做,结果是int
  • x = x<<1 等价于 x = x*2
  • x = x<<n 在范围内等价于x = x*(2的n次方)

移位运算:右移>>

  • i >> j
  • i中所有的位向右移j位
  • 所有小于int的类型,移位以int的方式来做,结果是int
  • 对于unsigned的类型,左边填入0
  • 对于signed的类型,左边填入原来的最高位
  • x = x>>1 等价于 x = x/2
  • x = x>>n 等价于 x = x/(2的n次方)

no zuo no die

  • 移位的位数不要用负数,这是没有定义的行为

利用按位与和移位操作实现输出一个数的二进制:

#include<stdio.h>

int main(int argc,int *argv[])
{
    int number;
    scanf("%d",&number);
    unsigned int mask = 1;
    mask = mask << 31;
    while(mask)
    {
        if(number & mask)
        {
            printf("1");
        }
        else
        {
            printf("0");
        }
        mask = mask>>1;
    }
    return 0;
}

 

位段

把一个int的若干位组合成一个结构

如:

struct U0
{
    unsigned int leading : 3;
    unsigned int FLAG1:1;
    unsigned int FLAG2:1;
    int trailing :27;
};

 

  • 这样就可以直接用位段的成员名称来访问,以移位、与、或更方便
  • 编译器会安排其中的位的排列,不具有可移植性
  • 当所需的位超过一个int时会采用多个int

 

文件输入输出

  • 用>和<做重定向
  • 使用FILE

FILE

  • FILE *fopen(const char * restrict path,const char * restrict mode);
  • int fclose(FILE *stream);
  • fscanf(FILE* , …)
  • fprintf(FILE* , …)

打开文件的标准代码

FILE *fp = fopen("file","r");//文件名,只读模式
if(fp){
   fscanf(fp,...);
   fclose(fp);
  }else{
   ...
}

 

例子:(打开当前.c源代码目录下的1.txt文件中的数字并输出到终端。)

#include<stdio.h>

int main(int argc,int *argv[])
{
    FILE *fp = fopen("1.txt","r");
    if(fp)
    {
        int num;
        fscanf(fp,"%d",&num);
        printf("%d\n",num);
        fclose(fp);
    }
    else
    {
        printf("无法打开文件\n");
    }
    return 0;
}

 

fopen

第一个字符串参数为文件名,第二个字符串参数为模式

  • r   打开只读
  • r+   打开读写,从文件头开始
  • w   打开只写。如果不存在则新建,如果存在则清空
  • w+   打开读写。如果不存在则新建,如果存在则清空
  • a   打开追加。如果不存在则新建,如果存在则从文件尾开始
  • 在上述后面可以加x,代表只新建,如果文件已存在则不能打开

二进制文件

  • 其实所有的文件最终都是二进制的
  • 文本文件无非是用最简单的方式可以读写的文件
  • 而二进制文件是需要专门的程序来读写的文件
  • 文本文件的输入输出是格式化,可能经过转码

文本文件VS二进制文件

  • 文本的优势是方便人类读写,而且跨平台
  • 文本的缺点是程序输入输出需要经过格式化,开销大
  • 二进制的缺点是人类读写困难,可能因为int的大小不一致,大小端等问题导致不跨平台
  • 二进制的优点是程序读写快

程序为什么要文件

  • 配置:Unix用文本,Windows用注册表
  • 数据:稍微有点量的数据都放数据库了
  • 媒体:通过二进制,现实是程序通过第三方库来读写文件,很少直接读写二进制文件了

printf()

格式:%[flags][width][.prec][hlL]type

flag

  • –   左对齐
  • +   在前面放+或者-
  • (space) 正数留空
  • 0   0填充

width和pres

  • number   最小字符数
  • *   下一个参数是字符数
  • .number   小数点后面的位数
  • .*   下一个参数是小数点后的位数

hlL

  • hh   单个字节
  • h   short
  • l   long
  • ll   long long
  • L   long double

type

  • i或d   int
  • u   unsigned int
  • o   八进制
  • x   十六进制
  • X   大写十六进制
  • f或F   float
  • e或E   指数
  • g或G   float
  • a或A   十六进制浮点
  • c   char
  • s   字符串
  • p   指针
  • n   读入/写出的个数

scanf()

格式:%[flag]type

flag

  • *   跳过
  • 数字   最大字符数
  • hh   char
  • h   short
  • l   long,double
  • ll   long long
  • L   long double

type

  • d   int
  • i   整数,可能为十六进制或者八进制
  • u   unsigned int
  • o   八进制
  • x   十六进制
  • a,e,f,g   float
  • c   char
  • s   字符串
  • […]   所允许的字符
  • p   指针

printf()和scanf()的返回值

  • 读入的项目数
  • 输出的字符数
  • 在要求严格的程序中,应该判断每次调用scanf()或printf()的返回值,从而了解程序运行中是否存在问题

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

一开始我们写的所有程序都在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);             
}

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