程序结构

逻辑结构

在一个C++程序中,main函数是整个程序的入口。main函数通过调用其他的函数完成一系列功能,最后正常情况下应从main函数退出。

工程文件

C++文件一般由头文件(.h)和源文件(.cpp)组成。

头文件是专门用来给其他文件引用使用的。通过#include引入头文件时,实际上只是将一个头文件的所有内容拷贝到引用的文件里。因此,如果把头文件中的内容全部手动拷贝到引用者那里,然后不引用头文件,也能达成完全一致的效果。

源文件里用来定义头文件中出现的那些变量和函数的具体实现。

一般来说,以下内容应该写入头文件中:

  • 常量定义
  • 宏(编译预处理语句)
  • 函数原型、变量的声明
  • 类声明
  • 内联函数
  • 如有需要,要在头文件中写明命名空间的定义

而函数的具体实现、变量的具体声明、类的方法具体实现都应该写在.cpp源文件中。一般来说不应该将函数的具体实现写入头文件中,如果这样做,编译器将默认把这个函数做成inline函数。

注意:头文件和源文件的名字相同是为了易读性高,事实上并不存在任何关联!也就是说完全可以在A.h中声明一个函数原型,然后在B.cpp中写实现,只不过这样可读性不高,所以不会这样做!

extern关键字

这个关键字放在全局变量和函数声明的前面,表示这里引入一个外部定义过的全局变量(或函数)。在头文件中声明希望被其他引用者使用的全局变量时,一般就需要通过extern关键字声明。一般具体的全局变量的定义在源文件中,如果这里不用extern,就等于重新声明了同名的变量,造成编译错误。

前面说过,头文件就是给引用者准备的。因此,如果把头文件中的内容全部手动拷贝到引用者那里,然后不引用头文件,也能达成完全一致的效果。所以可以理解,extern关键字引入的”外部的全局变量“,是基于引用者而言的。extern引入外部的全局变量后,在引用者这里可以如同自己声明的全局变量一样使用。

由于函数本身默认就可以被外部引用,所以函数原型声明前的extern关键字是可选的,写不写完全不影响效果。

例子:

// test.cpp
int a=1; //这里定义了全局变量a

// test.h
extern int a; // 这里表示在引用者那里,可以使用外部定义的a。

//main.cpp
#include <iostream>
#include "test.h" //效果等同于写一句extern int a;
int main(){
    std::cout<<a; //因为有外部声明,不会报错。a在这里是一个全局变量
}

注意:在不使用命名空间的情况下,外部声明有着不可控的因素,因为不知道引入的内容中会不会出现重名的变量。一旦有两个文件中用到了同一个变量名,使用外部声明这个变量的文件在编译时便会失败(死于重复定义)。

static关键字

static关键字有很多用途。在OO中,static可以类似java来使用,使得某个变量或方法归类所有。除此之外,如果不是在类中,static还有以下用法:

  • 放在变量声明前,使得这个变量从程序运行就被加载,到程序结束才释放,延长了存在的时间(但是依然不能超出作用域调用)
  • 放在全局变量前,使得这个全局变量不能被外部引用,即不能被其他文件通过extern外部声明。
  • 放在函数前,使得这个函数不能被外部文件调用。

例如

// test.cpp
static int a=1; //这里定义了全局变量a,并且用static修饰

// test.h
extern int a; // 错误!extern不能外部声明static的全局变量

//main.cpp
#include <iostream>
#include "test.h" //效果等同于写一句extern int a;
int main(){
    std::cout<<a; 
}

命名空间

正如上面所说,一个复杂的程序中往往会引入众多的外部变量和函数,而且面临着同名冲突的风险。因此命名空间就可以来避免冲突的命名带来的麻烦。命名空间分为定义和使用两部分。

定义

使用namespace关键字来定义一个命名空间,使用的方法如下:

namespace one {
    int a = 1;

    void a_print() {
        std::cout << "one: a" << std::endl;
    }
}

这样,命名空间大括号中间的所有函数和变量均属于one这个命名空间。

同一个命名空间可以多次定义,也可以在多个文件中定义,相当于对这个命名空间的补充。

命名空间可以进行嵌套定义,即在一个定义中再定义一个命名空间。使用的时候可以形如A::B::value这样。

使用

通过<命名空间名>::<变量/函数名>的方法来调用特定命名空间中的函数或者变量。如果::前面不加任何命名空间,则指的是这里的全局变量(可以是外部文件中的)。不使用::则优先匹配局部变量,如果不存在再匹配全局变量。

也可以使用using namespace <命名空间名>;的语句来为当前的作用域声明默认的命名空间,而这个效果一直贯穿整个当前的作用域,直到当前的这一块代码结束。虽然也可以在同一个作用域中使用多个using namespace,达到的效果是这个作用域之后使用的变量和函数会从这多个命名空间中去匹配,但是不推荐这样使用,因为这样仍会有命名冲突的风险。

例如:

#include <iostream>

namespace one {
    int a = 1;

    void a_print() {
        std::cout << "one: a" << std::endl;
    }
}

namespace one {
    int b = 2;

    void b_print() {
        std::cout << "one: b" << std::endl;
    }
}

namespace two{
    int a = 3;
    int c = 4;

    void a_print() {
        std::cout << "two: a" << std::endl;
    }
}

int main() {
    using namespace one;
    using namespace two;
    a=1; //出错!one和two中都有a的定义,不知道指的是哪个。
    c=4; //正常,因为只有two中有c,无冲突。
    return 0;
}

int main_2(){
    if (1){
        using namespace one;
        a=1;
    }
    if (1){
        using namespace two;
        a=2;//正常,因为using namespace one的作用域在上一个if块中就结束了,这里只能是two::a
    }
}

最后只需要知道,命名空间的目的是让编译器知道如果有重名的函数或变量,应该调用哪一个版本。

对于嵌套的命名空间来说,内层命名空间中的变量不属于外层命名空间,而外层命名空间中的变量也不属于内层。他们是两个独立的命名空间,只不过指定内层命名空间的时候必须借助外层来指定(相当于名字长了点而已)。

编译预处理(宏)

编译预处理的语句事实上会在编译之前由编辑器来执行完成,编辑器根据编译预处理的语句指定对代码内容进行一定的修改再交给编译器。所以看待宏一定要站在编辑文件的角度,无非是对代码进行拷贝、粘贴、删除的操作。

介绍几个常见的编译预处理语句。

#include

简单地把引入的文件内容复制过来,没有任何多余操作。

#define

定义一个编译预处理时用到的变量,语法为#define A B。也可以定义一个操作,比如#define mul(a,b) (a)*(b)

编辑器将文件中出现的匹配A的内容替换为B(类似手动查找替换,带点智能)。

#if #else #elif #endif

条件选择套件。

#if后面跟一个表达式,如果满足则将下面的内容放在文件中,否则移除下面的内容(到#endif为止)。#else和#elif用来协助。

#ifdef #ifndef

也是条件表达式。

#ifdef A,如果A之前被#define定义过,则为真,将下面的一块内容放入文件。如果A没被定义过就为假,移除下面一块内容。

#ifndef A,和#ifdef A恰好相反,如果A被定义过就为假,否则为真。