Skip to content

1. C++程序的组成

C++程序的组成主要包括以下几个部分:

1.1 预处理指令

C++程序中的预处理指令以井号(#)开头,如#include等。这些指令在程序编译前由预处理器处理,用于包含头文件等。

cpp
#include<iostream>

1.2 全局声明和定义

全局声明和定义通常位于程序的开头部分,包括数据类型声明、变量定义、函数原型声明等。这些声明和定义在整个程序中都是可见的。

cpp
#include <iostream>
int a = 1; //变量的定义
struct student //数据类型定义
{
    char name[10];
    int age;
    float height;
};

int add(int, int); //函数声明

int main()
{
    return 0;
}

1.3 函数

C++程序由若干个函数组成,每个函数用于实现特定的功能。函数之间可以相互调用,以实现复杂的逻辑。主函数(main函数)是程序的入口点,程序从这里开始执行。

cpp
#include <iostream>

int add(int a, int b)
{
    return a + b;
}

int main()
{
    int sum = add(1, 2); //main函数调用add函数
    std::cout << "sum = " << sum << std::endl;
    return 0;
}

1.4 语句和表达式

语句是C++程序的基本执行单元,用于实现各种操作,如赋值、调用函数、控制流程等。表达式则是由运算符和操作数组成的,用于计算结果或值。

cpp
#include <iostream>

int add(int a, int b)
{
    return a + b;
}

int main()
{
    int sum = add(1, 2);
    std::cout << "sum = " << sum << std::endl;

    int c = -1;
    std::cin >> c;
    if (c < 0)
    {
        c = -c;
    }
    std::cout << "c = " << c << std::endl;
    return 0;
}

1.5 对象和类

C++是一种面向对象的语言,因此对象和类是C++程序的重要组成部分。对象是类的实例,具有类的属性和方法。类定义了对象的结构和行为。

cpp
#include <iostream>
struct student //数据类型定义
{
    char name[10]; //属性
    int age;
    float height;

    void print() //方法
    {
        std::cout << "name = " << name << ", age = " << age << ", height = " << height << std::endl;
    }
};

int main()
{
    struct student stu = { "abc", 18, 181.1f };
    stu.print();
    return 0;
}

1.6 内存管理

C++程序需要管理内存,包括分配和释放内存。这可以通过使用指针、动态内存分配(如new和delete操作符)等来实现。

cpp
#include <iostream>
#include <string>
struct student //数据类型定义
{
    char name[10];
    int age;
    float height;

    void print()
    {
        std::cout << "name = " << name << ", age = " << age << ", height = " << height << std::endl;
    }
};

int main()
{
    struct student stu = { "abc", 18, 181.1f };
    stu.print();
    struct student* stu1 = new student;
    strcpy(stu1->name, "def");
    stu1->age = 20;
    stu1->height = 181.2f;
    stu1->print();
    delete stu1;
    stu1 = NULL;
    return 0;
}

1.7 异常处理

C++程序可以使用异常处理机制来处理运行时错误。通过try-catch块,程序可以在发生异常时捕获并处理错误。

cpp
#include <iostream>
#include <string>

struct student //数据类型定义
{
    char name[10];
    int age;
    float height;

    void print()
    {
        std::cout << "name = " << name << ", age = " << age << ", height = " << height << std::endl;
    }
};

int main()
{
    struct student stu = { "abc", 18, 181.1f };
    stu.print();
    struct student* stu1 = new student;
    if (!stu1)
    {
        std::cout << "申请内存失败" << std::endl; //异常处理
    }
    strcpy(stu1->name, "def");
    stu1->age = 20;
    stu1->height = 181.2f;
    stu1->print();

    delete stu1;
    stu1 = NULL;

    return 0;
}

这些组成部分共同构成了一个完整的C++程序。在实际编程中,还需要考虑程序的结构、可读性、可维护性等因素,以编写出高质量、易于理解和维护的代码。

1.1 宏

宏的定义

在C++中,宏(Macro)是预处理器的一部分,用于在编译之前对源代码进行文本替换。宏由预处理器#define指令定义,并且在编译之前由预处理器处理。宏通常用于定义常量、简化复杂的代码表达式等。

宏的定义方式

宏可以用来给数字起名字 定义宏的时候要把宏名称写在前面,把它代表的数字写在后面 宏名称通常由全大写英文字母构成 宏名称里不可以包含空格 用宏给数字起名字的时候不仅可以使用赋值操作符 编译器会把程序里的所有宏名称替换成它所代表的数字

cpp
#define 名字 数字

#define PI 3.14
//#define PI=3.14/error

可以在编译命令使用-D选项决定宏名称代表的数字

编写程序的时候有些数字只能在编译的时候才能知道具体数字,程序中应该用宏名称代表这种数字

bash
g++ -D PI=3.14 .\test.cpp -o test

1.1.1 宏函数

宏函数的定义

在定义宏函数的时候注意宏函数名和(之间不能有空格。

cpp
#define 宏函数名(参数) 计算公式

宏函数求圆的面积

cpp
#include <iostream>
#define PI 3.14
#define CIRCLE(r) 2 * PI * r

int main()
{
    float r = 1.0f;
    float area = CIRCLE(r);
    std::cout << "area = "<< area << std::endl;
    return 0;
}
  • 如果宏有多个参数也应该用逗号把相邻的参数名称分隔开
cpp
#include<iostream>
#define SUP(num1, num2) ((num1) - (num2))

int main()
{
    int num1, num2;
    std::cout << "请输入两个整数";
    std::cin >> num1 >> num2;
    std::cout << "结果是 "<< SUP(num1, num2) << std::endl;
    std::cout << "结果是 "<< 10 - SUP(num1, num2) << std::endl;
    std::cout << "结果是 "<< 10 - SUP(num1, num2 - 5) << std::endl;
    return 0;
}

1.1.2 函数和宏函数的区别

从上面可以看到函数式宏在某些时候可以替代函数的作用,它们的区别如下:

  • 宏函数是在编译时展开并填入程序的。而函数定义则需要为每个形参都定义各自的数据类型,返回值类型也只能为一种。函数更为严格。
  • 函数默认为我们进行一些复杂的操作,比如:
    • 参数传递(将实参值赋值给形参)
    • 函数调用和函数返回操作
    • 返回值的传递
cpp
int add(int a, int b) //将实参1,2值赋值给形参a,b
{
    return a + b;
}

int c = add(1, 2); //函数调用和返回,返回值的传递

而函数式宏只是做宏展开,并不做上述处理。

cpp
#define SUP(num1, num2) ((num1) - (num2)) //既可以用int类型计算,也可以用double类型运算

宏函数能是程序的运行速度稍微提高一点儿,但是当函数中有大量的宏替换的时候,又会使得程序变得臃肿。原理是宏函数只做替换,并没有类似函数调用的跳转、参数出入栈等操作,自然会提高函数的运行速度。这种运行速度加快体验在大量使用的时候尤为明显。

宏在使用的时候必须小心谨慎,避免出现问题。这一点是有宏的本身特性决定,即只做替换不做计算。

1.1.3 宏函数的副作用

  • 不要把自增或自减的结果作为宏的参数使用

在不同的编译器下结果不同 若调用该函数式宏计算sqr(a++),展开后就变为:(a++)*(a++),可以发现执行了两次自增操作。这就会造成隐形的错误,比如我只是想将a自增1后在求其平方,但是结果却并非我们所想。

cpp
#include<iostream>
#define sqr(a) ((a)*(a))

int main()
{
    int a = 10;
    std::cout << "result is " << sqr(a++) << std::endl; //(a++) * (a++)
    return 0;
}
  • 在定义函数式宏的时候与一定要每个参数以及整个表达式都用()括起来 宏只是做了简单替换
cpp
#include<iostream>
#define ADD(a, b) (a) + (b) //不规范的定义
#define MUL(a, b) (a * b) //不规范的定义

int main()
{
    std::cout << ADD(1, 2) * ADD(1, 2) << std::endl; //1 + 2 * 1 + 2 = 5
    std::cout << MUL(1 + 1, 2 + 2) << std::endl; //1 + 1 * 2 + 2 = 5
    return 0;
}
  • 宏函数也可以像函数那样不带参数
cpp
#include<iostream>
#define my_print() (std::cout << "你好啊!" << std::endl)

int main()
{
    my_print();
    return 0;
}
  • 编写宏的时候可以使用一些特殊的符号,它们叫做宏操作符

#是一个宏操作符,它可以把宏的一个参数转换成字符串字面值

##也是一个操作符,它可以把一个代表标识符的参数链接其他内容得到一个新的标识符

cpp
#include<iostream>
#define STR(n) #n
#define CONCAT(a, b) a##b

int main()
{
    std::string str = STR(2+3);
    std::cout << STR(2+3) << std::endl;
    std::cout << str << std::endl;
    std::cout << CONCAT("abc", "def") << std::endl;
    return 0;
}

虽然宏在某些情况下很有用,但它们也有一些显著的缺点:

没有类型检查:宏只是简单的文本替换,没有类型检查,这可能导致类型错误在编译时不会被捕获。

cpp
#include<iostream>
#define ADD(a, b) ((a) + (b))

int main()
{
    std::cout << ADD(2, "a") << std::endl;
    return 0;
}

作用域问题:宏没有作用域限制,一旦定义,它将在整个源文件中可用,这可能导致命名冲突。

不可调试:由于宏是预处理时展开的,调试器通常无法识别宏调用,这使得调试更加困难。

复杂的宏可能难以阅读和维护:复杂的宏可能包含很多逻辑和嵌套,这使得代码难以理解和维护

1.2 条件编译

格式(#ifdef(#ifndef) ... #else ... #endif)

cpp
#ifdef(#ifndef) 宏定义
...
#else
...
#endif

这个结构可以根据一个宏名称是否被定义过从两组语句中选择一组编译

最开始的预处理指令应该从两个里选择一个不管选择哪个都应该在后面写一个宏名称

如果最开始的预处理指令选择#ifdef就表示后面的宏名称被定义的时候编译前一组语句,否则编译后一组语句

示例:

cpp
#include<iostream>
//#define A
int main()
{
    //#ifdef A
    #ifndef A
    std::cout << "\A没有定义" << std::endl;
    #else
    std::cout << "\A定义了" << std::endl;
    #endif
    return 0;
}
  • 格式(#if ... #elif(任意多次)... #else ... #endif)
cpp
#if 表达式
#elif 表达式
...
#else
#endif

#if...#elif(任意多次)...#else...#endif

以上结构也可以实现条件编译的效果,它可以根据任意逻辑表达式从多组语句中选择一组编译

#if和#elif后都需要写逻辑表达式,这些逻辑表达式里可以使用任意逻辑操作符,这些逻辑表达式的作用和行分支逻辑表达式的作用一样

cpp
#include<iostream>
#define A -1

int main()
{
    #if A > 0
    std::cout << "正数" << std::endl;
    #elif A == 0
    std::cout << "0" << std::endl;
    #else
    std::cout << "负数" << std::endl;
    #endif
    return 0;
}

1.3 多文件编程

多文件编程的时候,任何一个函数只能属于一个文件,一个文件可以包含多个函数

1.3.1 多文件编程基本过程

  1. 把所有函数分散在多个不同的源文件里(主函数通常单独在一个文件里)

这些文件包含类的实现、函数的定义、变量的定义等。它们是程序的主体部分,包含了程序的实际逻辑、源文件通常以 .cpp 作为文件扩展名。

  1. 为每个源文件别写配对的以.h作为扩展名的头文件(主函数所在的源文件不需要配对的头文件)这些文件包含类的声明、函数原型、变量声明、宏定义和常量等。它们的主要作用是提供接口,让其他源文件知道如何使用这些声明和定义。头文件通常以 .h 或 .hpp 作为文件扩展名。

只要不分配内存的内容都可以写在头文件里,头文件里至少应该包含配对源文件里的所有函数声明

在test_1.h文件中写函数的声明

cpp
void print();
int add(int a, int b);

3.修改所有源文件,在源文件里使用#include预处理指令包含必要的头文件(在.h中写函数的声明,在对应的.cpp文件中写函数的实现)

在多文件编程中,头文件和源文件之间通过#include指令相互引用。头文件中的声明告诉编译器如何在源文件中找到和使用这些声明,而源文件中的定义则提供了这些声明的具体实现。

(配对头文件时必要头文件,如果源文件里使用了某个头文件里声明的函数,则这个头文件也是必要头文件)

在在test_1.cpp文件中写函数的定义

cpp
#include"test_1.h"
#include<iostream>
void print()
{
    std::cout << "test_1.cpp中的print函数" << std::endl;
}

int add(int a, int b)
{
    return a + b;
}

主文件 (.cpp) 是程序的入口点,通常包含一个 main 函数。主文件负责将其他源文件编译链接在一起,形成最终的可执行程序。

在test.cpp文件中main主函数,调用包含的test_1.h头文件(通常值包含.h文件)

cpp
#include<iostream>
#include"test_1.h"

int main()
{
    print();
    int a = add(1, 2);
    std::cout << a << std::endl;
}

多文件编程时编译的命令:编译多文件程序的时候需要在g++命令后列出所有源文件的路径

bash
g++ 所有的源文件目录 -o 生成的可执行程序名
bash
PS D:\Rescours\C++\Demo\Day01Demo_C++> g++ .\test.cpp .\test_1.cpp -o test
PS D:\Rescours\C++\Demo\Day01Demo_C++> .\test.cpp
PS D:\Rescours\C++\Demo\Day01Demo_C++> .\test.exe
test_1.cpp中的print函数

1.3.2 extern关键字

如果希望从一个源文件里使用另外一个源文件里生命的全局变量就需要使用extern关键字再次声明这个全局变量,这种使用extern关键字声明变量的语句不会分配内存,它们通常写在头文件里。

在test.cpp中有全局变量int num,在test_1.cpp文件中对该变量进行赋值操作

test.cpp

cpp
#include<iostream>
#include"test_1.h"

int num = 100;

int main()
{
    print();
    int a = add(1, 2);
    std::cout << a << std::endl;
    std::cout << "&num = " << &num << "--by main" << std::endl;
    setvalue();
    std::cout << "num = " << num << std::endl;
}

test_1.h

cpp
void print();
int add(int a, int b);
extern int num;
void setvalue();

test_1.cpp

cpp
#include"test_1.h"
#include<iostream>

void print()
{
    std::cout << "test_1.cpp中的print函数" << std::endl;
}

int add(int a, int b)
{
    return a + b;
}

void setvalue()
{
    std::cout << "&num = " << &num << "--by setvalue" << std::endl;
    std::cout << "请输入一个数字。";
    std::cin >> num;
}

不可以跨文件使用静态全局变量

cpp
#include<iostream>
#include"test_1.h"

/*static*/ int num; //error static变量的作用域限定在本文件中

int main()
{
    print();
    int a = add(1, 2);
    std::cout << a << std::endl;
    std::cout << "&num = " << &num << std::endl;
    setvalue();
    std::cout << "num = " << num << std::endl;
}

1.3.3 头文件重复引用

比如,test_1.h包含了test_2.h,而test_2.h又包含了test_1.h。这导致了一个循环依赖,并且每次包含任何一个头文件时,都会无限递归地包含另一个头文件。这就叫做头文件重复引用。

这个过程会无限重复,直到预处理器达到其递归深度限制,从而导致编译错误。

即使不出现无限递归,头文件重复包含也可能导致其他问题,比如:

  1. 重复定义相同的函数或类。
  2. 增加编译时间,因为相同的代码被多次包含。
  3. 可能导致宏的重复扩展,从而产生不期望的副作用。

为了避免这些问题,我们总是在头文件的开头使用包含保护,即使用条件编译。

  • 解决头文件重复引用时的条件编译命令格式(建议)
cpp
#ifndef 头文件名(全大写,用_连接)
#define 头文件名

//包含的头文件和函数声明

#endif

test_1.h

cpp
#ifndef __TEST_1_H__
#define __TEST_1_H__

#include"test_2.h"

void print();
int add(int a, int b);

#endif

test_2.h

cpp
#ifndef __TEST_2_H__
#define __TEST_2_H__

#include"test_1.h"

void print2();
int sub(int a, int b);

#endif

这样以来就完美解决了头文件重复引用的问题。可以在主函数中调用这两个文件中的函数

cpp
#include<iostream>
#include"test_1.h"
#include"test_2.h"

int main()
{
    print();
    print2();
    int a = add(1, 2);
    cout << sub(3, 4) << endl;
    std::cout << a << std::endl;
}

2. 编译过程

可以分为以下四个主要阶段:

2.1 预处理(Preprocessing)

在预处理阶段,预处理器(preprocessor)会处理源代码中的预处理指令(例如 #include、#define 等)。预处理器的工作包括:

  • 展开 #include 指令,将包含的头文件内容插入到源代码中。
  • 处理 #define 指令,替换宏定义。
  • 处理条件编译指令(如 #ifdef、#ifndef、#if 等)。
  • 删除注释和空白字符等。

预处理后的输出通常称为预处理文件(.i文件)。

● 编译指令

bash
gcc -E 源文件名.c -o 源文件名.i

test.cpp

cpp
#include <iostream>
#include"test_1.h"
#define PI 3.14

int main()
{
    float area = PI * 2 * 2;
    //求半径为2的圆的面积
    std::cout << "area = " << area << std::endl;

    //定义一个变量
    int i = "abc";
    return 0;
}

test_1.cpp

cpp
#include"test_1.h"
#include<iostream>
void print()
{
    std::cout << "test_1.cpp中的print函数" << std::endl;
}

test_1.h

cpp
#ifndef __TEST_1_H__
#define __TEST_1_H__
void print();
#endif

预处理命令

bash
g++ -E test.cpp -o test.i

可以看到将头文件完成了包含、宏定义完成了替换、处理条件编译指令、删除注释和空白字符等。

而且将 int i 用 string abc 赋值并不报错。因为在这一步并不检查语法错误。

2.2 编译 (Compilation)

编译阶段是将预处理后的代码转换成汇编代码。在这个阶段,编译器(compiler)会检查源代码的语法和语义,并将其转换为汇编语言代码。编译器会生成一个或多个汇编文件(.s文件)。

如果源代码中存在语法错误或类型不匹配等问题,编译器会在这个阶段报错。

test.cpp

cpp
#include <iostream>
#include"test_1.h"
#define PI 3.14

int main()
{
    float area = PI * 2 * 2;
    //求半径为2的圆的面积
    std::cout << "area = " << area << std::endl;

    //定义一个变量
    int i = "abc";
    double b;
    return 0;
}

编译指令

bash
g++ -S 源文件名.i -o 源文件名.s
g++ -S test.i -o test.s

此时检查语法错误和和类型不匹配问题。

bash
P5 D:\Rescourse\C++\Demo\Day@IDemo_C++> g++ -S test.i -o test.s
.ltest.cpp: In function 'int main()';
.ltest.cpp:13:13; error: invalid conversion from 'const char*' to 'int' [-fpemissive]
    int i = "abc";
.ltest.cpp:15:5; error: expected initializer before 'return'
    return 0;
P5 D:\Rescourse\C++\Demo\Day@IDemo_C++>

修改完编译错误可发现,生成的test.S文件中将C++语言翻译成了汇编语言

2.3 汇编 (Assembly)

汇编阶段是将编译阶段生成的汇编代码转换为机器代码(也称为目标代码)。汇编器(assembler)会读取汇编文件,并将其转换为目标文件(.o文件或.obj文件)。

汇编指令

bash
g++ -C 源文件名.s -o 源文件名.o

此时生成的是机器语言,目标文件是机器语言代码的文件,它们可以直接由计算机执行,但通常还需要进一步的链接才能形成可执行程序

2.4 链接(Linking)

链接阶段是将一个或多个目标文件以及可能需要的库文件合并成一个可执行文件(.exe文件)。链接器(linker)负责解析目标文件之间的相互引用,例如函数和变量的调用。

如果目标文件之间有未解析的符号引用(例如,一个目标文件调用了另一个目标文件中定义的函数,但链接器找不到该函数的定义),链接过程会失败,并报告链接错误。

  • 链接命令
    • g++ 源文件名.o -o 源文件名
    • g++ 源文件名.o -o 源文件名
    • g++ 源文件名.o -o 源文件名

完成链接后,就得到了一个可以直接运行的可执行文件。整个编译过程可能由单个命令(如 g++ test.cpp -o test)来触发,但这个命令背后实际上是调用了预处理器、编译器、汇编器和链接器这一系列工具。

3. 动态库和静态库

3.1 动态库

库其实代码的一种二进制的封装形式。在其他的源码中,是可以直接调用,但是看不到具体的实现方式。

库的封装有利于模块化的设计,而且只要接口设计的合理,改变库的实现,是不影响使用库的代码。

编译速度

使用库可以加快编译速度。这是因为库中的代码只需要编译一次,然后就可以被多个程序重复使用,而不需要每次重新编译。

模块化

可以帮助实现代码的模块化。这意味着可以将大型程序分解为更小的、更易于管理的部分。这有助于提高代码的可维护性和可重用性。

隐藏实现细节:

隐藏实现细节,只向用户提供必要的接口。这有助于保护代码的安全性,防止未授权访问。

3.1.1 动态库的定义

动态库(也称为共享库)是一种在运行时被程序加载和链接的二进制文件,它包含了程序执行时需要的代码和数据。动态库在程序运行时才被加载到内存中,而不是在编译时。这使得多个程序可以共享同一个动态库的内存副本,从而节省内存空间。

在C++中,动态库通常具有 .so(在Linux和Unix系统中)或 .dll(在Windows系统中)的文件扩展名。

3.1.2 动态库的编译过程

要创建动态库,你需要将你的C++代码编译为目标文件(.o),然后使用链接器将其转换为动态库。这个过程通常涉及以下几个步骤:

  • 编写源代码:首先,你需要编写你的C++源代码。这些代码将包含你想要在动态库中提供的函数和类的定义。
    • o:一般来说,在库文件源代码中,是不会包含main函数的。

test_1.h

cpp
#ifndef __TEST_1_H__
#define __TEST_1_H__
void print();
int myadd(int, int);
typedef struct student
{
    char name[10];
    int age;
    float height;
    void print();
} STUDENT;

#endif

test_1.cpp

cpp
#include "test_1.h"
#include <iostream>

void print()
{
    std::cout << "test_1.cpp中的print函数" << std::endl;
}

int myadd(int a, int b)
{
    return a + b;
}

void STUDENT::print()
{
    std::cout << "name = " << name << ", age = " << age << ", height = " << height << std::endl;
}

● 编译源代码:使用C++编译器(如g++)将源代码编译为与位置无关的目标文件。

bash
g++ -c -fPIC 源文件名.cpp -o 源文件名.o

这里,-c选项告诉编译器只进行编译而不进行链接,-fPIC选项生成位置无关的代码,这对于动态库是必要的。

bash
g++ -c -fPIC .\test_1.cpp -o test_1.o

● 创建动态库:使用链接器将目标文件转换为动态库。在Linux或Unix系统中,你可以使用以下命令:

bash
g++ -shared 源文件名.o -o lib源文件名.so

在Windows系统中,你可以使用以下命令:

bash
g++ -shared 源文件名.o -o 源文件名.dll

生成test_1.dll

bash
g++ -shared test_1.o -o test_1.dll

这些命令将目标文件mylib.o链接为一个名为libmylib.so(或mylib.dll)的动态库。

3.1.3 动态库的使用

● 在你的C++应用程序中,你需要包含共享库的头文件,并在编译时链接到共享库。

cpp
#include <iostream>
#include "test_1.h" //包含头文件

int main()
{
    print();
    std::cout << myadd(1, 2) << std::endl;
    STUDENT stu = {"abc", 18, 181.1f};
    stu.print();
    return 0;
}

● 编译代码

● o 编译应用程序时,需要指定共享库的位置:

其中-L选项指定了库文件的路径,-l选项指定了库的名字(不包括前缀lib和后缀.so)。

bash
g++ 源文件名.cpp -L库文件目录 -l库文件名 -o 源文件名
g++ test.cpp -L / -ltest_1 -o test
  • o 将动态库路径添加到环境中(仅linux/unix系统生效)

运行代码时,确保动态库文件与可执行文件位于同一目录中,或者共享库位于系统的库路径中(如linux下:/usr/lib或/usr/local/lib)。你可以通过设置LD_LIBRARY_PATH环境变量来指定额外的库路径。

bash
export LD_LIBRARY_PATH=:$LD_LIBRARY_PATH::动态库路径
./源文件名

这样,你的C++应用程序就可以在运行时动态地链接到并使用。

3.2 静态库

3.2.1 静态库的定义

静态库(Static-Library)是一组预先编译好的方法的集合,通常以.a(在Unix系统中)或.lib(在Windows系统中)为扩展名。静态库在程序编译时被完整地拷贝到最终的可执行文件中,因此最终生成的文件会比较大。与动态库(Dynamic Library)不同,静态库在程序运行时不需要额外加载,因为所有需要的代码都已经编译时被包含进了可执行文件。

3.2.2 静态库的编译过程

静态库编译过程通常涉及以下几个步骤:

  • 编写源代码:首先,你需要编写你的C++源代码。这些代码将包含你想要在动态库中提供的函数和类的定义。
  • o 注意:一般来说,在库文件源代码中,是不会包含main函数的。

test_l.h

cpp
#ifndef __TEST_L_H__
#define __TEST_L_H__

void print();
int myadd(int, int);

typedef struct student
{
    char name[10];
    int age;
    float height;
    void print();
} STUDENT;

#endif

test_l.cpp

cpp
#include "test_l.h"
#include <iostream>

void print()
{
    std::cout << "test_1.cpp中的print函数" << std::endl;
}

int myadd(int a, int b)
{
    return a + b;
}

void STUDENT::print()
{
    std::cout << "name = " << name << ", age = " << age << ", height = " << height << std::endl;
}

● 编译源代码:使用C++编译器(如g++)将源代码编译成目标文件。

bash
g++ -c -o 源文件名.o 源文件名.cpp

这里,test_1.cpp是源代码文件,test_1.o是生成的目标文件。

bash
g++ -c -o test_1.o test_1.cpp

● 创建静态库:使用ar命令将目标文件打包成静态库。

这里,test_1.lib是生成的静态库文件,test_1.o是之前生成的目标文件。r表示插入或替换现有的模块,c表示创建库(如果它不存在),s表示创建对象文件索引。

bash
ar rcs 源文件名.lib 源文件名.o

生成test_1.lib

bash
ar rcs test_1.lib test_1.o

3.2.3 静态库的使用

在应用程序中使用静态库时,你需要在编译时链接到该库。(不包括后缀 .lib)。例如:

bash
g++ 源文件名.c -L静态库路径 -l静态库名 -o 源文件名
g++ test.cpp -L / -ltest_1 -o test.o
g++ test.cpp -L / -ltest_1 -o test

3.3 静态库与动态库的区别

静态库和动态库是两种不同的库类型,它们在程序编译和运行时的行为有所不同。以下是它们之间的主要区别:

  1. 链接时间:

    • 静态库:在编译时链接。当程序编译时,静态库中的代码会被完整地拷贝到最终的可执行文件中。
    • 动态库:在运行时链接。当程序运行时,动态库会被加载到内存中,程序可以调用其中的函数或访问其中的数据。
  2. 可执行文件大小:

    • 静态库:由于静态库在编译时被完整地拷贝到可执行文件中,因此最终生成的可执行文件会比较大。
    • 动态库:由于动态库在运行时才被加载,因此最终生成的可执行文件通常比较小。
  3. 运行时依赖:

    • 静态库:静态库在编译后已经完全包含在可执行文件中,因此运行时不需要额外的库文件。
    • 动态库:动态库在运行时需要加载对应的库文件,如果库文件不存在或版本不匹配,程序可能无法正常运行。
  4. 更新和维护:

    • 静态库:如果静态库更新,需要重新编译和链接所有使用该库的程序。
    • 动态库:如果动态库更新,只需要替换掉原来的库文件,而不需要重新编译和链接程序。这使得动态库更加便于维护和更新。
  5. 内存占用:

    • 静态库:静态库中的代码在程序加载时会被加载到内存中,即使某些代码可能不会被执行。
    • 动态库:动态库中的代码只有在程序调用时才会被加载到内存中,这有助于减少内存占用。
  6. 平台兼容性:

    • 静态库:由于静态库在编译时被完全拷贝到可执行文件中,因此通常具有较好的平台兼容性。
    • 动态库:动态库可能依赖于特定的操作系统或平台,因此在不同的平台上可能需要不同的库文件。

总的来说,静态库和动态库各有优缺点,选择使用哪种类型的库取决于具体的项目需求和目标。在一些情况下,可能会选择静态库来简化部署和减少运行时依赖;而在其他情况下,可能会选择动态库来减少可执行文件大小、便于更新和维护。

4. VS2022创建库并调用

  1. 首先创建一个空项目,以Test为例

  2. 为该项目添加头文件和源文件(以test.h和test.cpp为例)

test.h

cpp
//#pragma once
#ifndef __TEST_H__
#define __TEST_H__

struct Test
{
    void say();
};

void print();

#endif // !_TEST_H__

test.cpp

cpp
#include "test.h"
#include<iostream>

void Test::say()
{
    std::cout << "Hello! This is struct Test!" << std::endl;
}

void print()
{
    std::cout << "Hello world!" << std::endl;
}
  1. 右键项目名,修改项目属性,生成对应的.lib文件

随后台键项目名生成

  1. 重新创建工程,然后右键属性,将test.h文件路径包含进该项目中

  2. 依然再该界面,将test.lib文件的路径也包含进来

  3. 依然在该界面,将test.lib文件添加到附加依赖项中

  4. 在该项目中添加源文件调用生成的库文件,以test.cpp为例,调用该库文件验证

test.cpp

cpp
#include <iostream>
#include "test.h" //将头文件添加进来
using namespace std;

int main()
{
    Test t;
    t.say();
    print();
    return 0;
}

至此,即可看到调用成功。该方法是静态库引用,动态库引用比较复杂,待日后有用到时再行探索

知识如风,常伴吾身