4、C/C++ 概述


一、C 和 C++的区别

  • C 是面向过程的编程语言,C++ 是面向对象的编程语言(相比 C 添加了类和模版等),支持泛型编程
  • C 语言中用来做控制输入输出的是stdio函数库,而 C++ 中为 iostream
  • C语言中输入输出使用scanf()printf() ,而 C++ 中为 cincout
  • C 语言中的换行符为 \n,而 C++ 中的换行符为 endl
  • C 语言中使用结构体时需要加上前缀 struct, 而在 C++ 中则不用
  • C 语言中的mallocfree对内存进行分配,C++ 中仍然支持,同时增加了 newdelete来管理内存(C++ 中的 new 操作符在默认情况下不会进行零初始化,但可以通过在 new 后加括号 () 或花括号 {} 来实现零初始化,使得分配的内存被初始化为零值或默认值)
  • C++允许我们在程序使用之前的任意位置声明变量,而在 C 语言中,必须要在函数开头部分
  • C++ 允许函数名重载,不过它们的参数个数或类型不能完全相同,而这在 C 语言中是不允许的
  • C 是 C++ 的子集,学习 C++ 就是先学 C 后学 ++
  • C++ 引入了异常处理和引用
  • C 中的空指针常用 NULL,C++ 中的空指针常用 nullptr
  • C 中无 bool 数据类型,需要引入 <stdbool.h> 头文件来使用,而 C++ 中有 bool 类型,取值为 true or false
  • C++11 开始,可以使用 auto 关键字通过初始化表达式自动推导对象类型,使用 auto 的时候必须对变量进行初始化,且其不能在函数的参数/模板参数/类的非静态成员变量中使用
    • auto varname = value; // auto 的语法格式,auto 根据=右边的初始值 value 推导出变量的类型;
    • decltype(exp) varname [= value]; //decltype 的语法格式,decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系。

1.1、C++对 C 的扩展

a、作用域运算符(::)
  • 作用域运算符可以用来解决局部变量与全局变量的重名问题
  • 在局部变量的作用域内,可用::对被屏蔽的同名的全局变量进行访问
// 全局变量
int a = 10;

// 1. 局部变量和全局变量同名
void test(){
	int a = 20;
	
	// 打印局部变量 a
	cout << "局部变量a:" << a << endl; // 局部变量a:20 
	
	// 打印全局变量 a
	cout << "全局变量a:" << ::a << endl; // 全局变量a:10
}
b、namespace
  • 定义:命名空间指标识符的各种作用域,把单个标识符下的大量有逻辑联系的程序实体组合到一起
  • 用途:解决命名冲突问题
  • 用法:
    • 命名空间必须在全局作用域下声明
    • 命名空间下可以放变量、结构体、函数、类...等等
    • 命名空间可以进行嵌套使用
    • 命名空间是开放的,可以随时加入新的成员
    • 命名空间可以起别名
#include <iostream>
using namespace std;

// 命名空间可以嵌套
namespace A {
    int a = 10;
    namespace B {
        int a = 20;
    }
}

void test() {
    cout << "A::a is: " << A::a << endl;  // A::a is: 10
    cout << "A::B::a is: " << A::B::a << endl;  // A::B::a is: 20
}

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


// 命名空间是开放的,可以随时把新成员加入已有命名空间中
namespace A {
    int a = 10;
}

namespace A {
    void func() {
        cout << "hello namespace!" << endl;
    }
}

void test() {
    cout << "A::a is: " << A::a << endl;  // A::a is: 10
    A::func();  // hello namespace!
}


// 无名命名空间,意味着命名空间中的标识符只能在本文件内访问,相当于给这个标识符加上了 static,使得其可以作为内部连接
namespace {
	int m_C = 0;  // 相当于定义了 static int m_C = 0 ; static int m_D = 0;
	int m_D = 0;
}

// 声明和实现的分离
namespace MySpace {
    void func1();
    void func2(int param);
}

void MySpace::func1() {
    cout << "MySpace::func1" << endl;
}

void MySpace::func2(int param) {
    cout << "MySpace::func2: " << param << endl;
}
c、using
  • using 声明:使得指定的标识符可用
  • using 编译指令:使整个命名空间标的识符可用
  • using 定义别名:可替代 typedef 来使用
  • Note:使用 using 声明using 编译指令会增加命名冲突的可能性,但若有命名空间,并在代码中使用作用域运算符,则不会出现二义性
#include <iostream>

using namespace std;

namespace A {
    int paramA = 20;
    void funcA() { cout << "hello funcA" << endl; }
}

void test1() {
    // using 声明
    using A::paramA;
    using A::funcA;
    cout << paramA << endl;  // 20
    funcA();  // hello funcA
    // int paramA = 20; // 在此作用域内已经声明过此变量, 会引起同名冲突
}

void test2() {
	// using 编译指令
    using namespace A;
    cout << "namespace A paramA is: " << paramA << endl;  // 20

    int paramA = 30;  // 在两个不同命名空间中定义的,采用就近原则,不会产生二义性
    cout << "namespace test paramA is: " << paramA << endl;  // 30
}

int main() {
    test1();
    test2();
    return 0;
}

// 重定义 unsigned int
typedef unsigned int uint_t;
using uint_t = unsigned int;

// 使用 using 别名语法定义了 std::map 的模板别名 str_map_t
template <typename Val>
using str_map_t = std::map<std::string, Val>;
// ...
str_map_t<int> map1;

1.2、C++对 C 的增强

a、变量检查、函数检查、类型转换检查
int a;
int a = 10;  // C++ 中会报重复定义错误

// C++ 会对返回值、参数类型、传参个数等做检查
int getRectS(int w, int h)
{
	return w*h;
}
void test02()
{
	getRectS(10, 10);
}

// calloc 返回 void* ,C 中可以不用强转,C++ 必须强转
char* p = (char*)calloc(1sizeof(64));
  • 类型转换
    • 定义:将一种数据类型转换成另一种数据类型
    • 例如:将一个整型值赋给一个浮点类型的变量,编译器会暗地里将其转换成浮点类型
    • 问题:在转换指针时,我们很可能将其转换成一个比它更大的类型,这可能会破坏其他的数据
    • 解决:使用 C++ 风格的显式强制转换,它可以更好的控制各种不同种类的强制转换,更清晰的表明它们要干什么
// 一、静态转换 static_cast< 目标类型>(原始数据):
// - 可以进行基础数据类型转换,如把 char 转换成 int
// - 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的
// - 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查, 所以是不安全的
// - 没有继承关系的自定义类型不可以转换

char a = 'a';
double b = static_cast<double>(a);  // 基础数据类型转换

// 继承关系指针转换
Animal* animal01 = NULL;
Dog* dog01 = NULL;
Animal* animal02 = static_cast<Animal*>(dog01);  // 子类指针转成父类指针,安全
Dog* dog02 = static_cast<Dog*>(animal01);  // 父类指针转成子类指针,不安全


// 二、动态转换 dynamic_cast< 目标类型>(原始数据):
// - 不可以转换基础数据类型
// - 进行上行转换时(把派生类的指针或引用转换成基类表示),dynamic_cast 和 static_cast 的效果是一样的
// - 没有继承关系的自定义类型不可以转换,不可以进行下行转换(把基类指针或引用转换成派生类表示),dynamic_cast 具有类型检查的功能
// - 发生多态时,上下行转换均可以
Animal* animal01 = NULL;
Dog* dog01 = new Dog;
Animal* animal02 = dynamic_cast<Animal*>(dog01);  // 子类指针转换成父类指针,可以
// Dog* dog02 = dynamic_cast<Dog*>(animal01);  // 父类指针转换成子类指针,不可以


// 三、常量转换 const_cast< 目标类型>(原始数据)
// - 常量指针(引用)被转化成非常量指针(引用),转换后仍然指向原来的对象
// - 不能直接对非指针(引用)的变量使用 const_cast 直接移除它的 const
const int* p = NULL;
int* np = const_cast<int*>(p);  // 去除 const
int* p2 = NULL;
const int* np2 = const_cast<const int*>(p2);  // 添加 const

const int a = 10;
// int b = const_cast<int>(a);  // 不能对非指针或非引用的变量进行转换

// 常量引用转换成非常量引用
int num = 10;
int & refNum = num;
const int& refNum2 = const_cast<const int&>(refNum);
b、struct 增强
  • C++ 结构体变量定义时,不用加 struct 关键字
  • C++ 结构体成员中,可以添加函数
struct Person
{
	int m_Age;
	void plusAge(){ m_Age++; }; // c++ 中 struct 可以加函数
};

void test()
{
	Person p1; // 使用时候可以不加 struct 关键字
	p1.m_Age = 10;
	p1.plusAge();
	cout << p1.m_Age << endl;
}
c、bool 数据类型增强
  • C 语言中也有 bool 类型,但需要通过包含头文件 stdbool.h 来使用
bool flag = true; // 只有真或假, true 代表真(非0),false 代表假(0)
void test()
{
	cout << sizeof(bool) << endl;  // 1
	flag = 100;
	cout << flag << endl;  // 1,bool 类型,非 0 的值都会转为 1
}
d、三目运算符增强
  • C 语言三目运算表达式返回值为数据值,为右值,右值为 Rvalue,R 代表 Read,可以知道它的值,不能赋值
  • C++ 三目运算表达式返回值为变量本身(引用),为左值,左值为 Lvalue,L 代表 Location,表示内存可以寻址,可以赋值
void test()
{
    int a = 10;
    int b = 20;
    cout << "b:" << b << endl;
    (a > b ? a : b) = 100;  // C++ 返回的是左值,变量的引用;C 中不可如此操作
	// *(a > b ? &a : &b) = 100;  // C 语言中模仿 C++ 可以这样写
    cout << "b:" << b << endl;
}
e、const 增强
  • C 中的 const:
    • C 中的 const 总是需要一块内存空间,它是一个伪常量,可以通过指针修改
    • C 中的 const 默认为外部连接,当两个文件中有同名只读变量时,编译器会报重定义的错误
// 1、全局 const:存储在只读数据段,不可修改
const int constA = 10;

int main() {
    int* p = (int*) &constA;
    *p = 200;  // Signal: SIGSEGV (Segmentation fault)
    return 0;
}

// 2、局部 const:存储在栈区,不能通过变量直接修改 const 只读变量的值,但是可以跳过编译器的检查,通过指针间接修改只读变量的值
int main() {
    const int constA = 10;
    // constA = 20;  // 报错,不能通过变量直接修改

    int *p = (int *) &constA; 
    *p = 300;
    printf("constA:%d\n", constA);  // constA:300
    printf("*p:%d\n", *p); // *p:300
    return 0;
}

// 3、可以用 const 常量定义数组
const int arrSize = 10;  // 编译器会把它放到符号表中,不分配内存
int arr[arrSize];  // 可以正常编译
  • C++ 中的 const:
    • C++ 中的 const 会放入符号表中,不必创建内存空间;而当取一个 const 地址, 或者把它定义为 extern,则会为该 const 创建临时内存空间
    • C++ 中的 const 默认为内部连接,当两个文件中有同名只读变量时,编译器不会报重定义的错误
    • 在 C++ 11 标准中,const 用于为修饰的变量添加“只读”属性;而 constexpr 关键字则用于指明其后是一个常量(或者常量表达式),编译器在编译程序时可以顺带将其结果计算出来,而无需等到程序运行阶段,这样的优化极大地提高了程序的执行效率
// 1、全局 const:存储在只读数据段,不可修改
const int constA = 10;

int main() {
    int* p = (int*) &constA;
    *p = 200;  // Signal: SIGSEGV (Segmentation fault)
    return 0;
}

// 2、局部 const:存储在栈区,不能通过变量直接修改 const 只读变量的值,但是可以跳过编译器的检查,通过指针间接修改只读变量的值
int main() {
    const int constA = 10;
    // constA = 20;  // 报错,不能通过变量直接修改 const 只读变量的值

    int *p = (int *) &constA;       // 对变量取地址,会分配临时内存
    *p = 300;
    cout << "constA:" << constA << endl;  // constA:10,编译器会把 constA 放到符号表中,不分配内存
    cout << "*p:" << *p << endl;  // *p:300
    return 0;
}

// 3、不可以用变量(包括只读变量)定义数组
const int arrSize = 10;  // 只读变量,占用内存
int arr[arrSize];  // 报错,编译器在编译时,不知道 arrSize 的值是多少


// 4、constexpr 修饰普通变量,此外还可以用于修饰函数(包括模板函数)以及类的构造函数
constexpr int num = 1 + 2 + 3;
int url[num] = {1,2,3,4,5,6};

在这里插入图片描述

  • 尽量用 const 代替 define:
    • const 有类型,可进行编译器类型安全检查,而 #define 无类型,不进行类型检查
    • const 有作用域,而 #define 不重视作用域,默认定义处到文件结束或者到 #undef
#define MAX 1024;      // 在预处理阶段被替换成 1024,编译器看不到 MAX
const int MAX = 1024;  // MAX 会加入到符号表中,编译器可以看到

1.3、引用(C++ 中新增,C 中无)

a、引用基本语法
  • 引用的定义:Type& Ref = Variable& 写到变量左侧叫引用,写到右侧叫取地址
    • 引用就是为已定义变量起个别名
    • 变量名实质上是一段连续内存空间的标号;程序中通过变量来申请并命名内存空间,通过变量的名字即可使用内存空间
  • 引用必须初始化,且初始化后不能修改;必须确保引用的是一块合法的内存(不能是 NULL
int a = 10;
int A = 20;

int& b = a;  // 引用必须初始化,给变量 a 取一个别名 b,与 a 共用一段内存
// int& b = A;  // 报错,初始化后不可修改指向。但可以操作 b 或 a 可以改变其值
int c = a;   // 把 a 的值赋给 c,c 会另外开辟一块内存

b = 100;     // 操作 b 就相当于操作 a 本身(指向同一块内存),此时 a,b 均为 100

// 数组中的引用
int arr[10] = {0};
int(&f)[10] = arr;  //  注意,需要加 [10]
for (int i = 0; i < 10; i++){
	f[i] = i + 10;
}
  • 函数中的引用
// 值传递
void ValueSwap(int m, int n) {
    int tmp = m;
    m = n;
    n = tmp;
}

// 地址传递
void PointerSwap(int *m, int *n) {
    int tmp = *m;
    *m = *n;
    *n = tmp;
}

// 引用传递
void ReferenceSwap(int &m, int &n) {
    int tmp = m;
    m = n;
    n = tmp;
}

void test() {
    int a = 10;
    int b = 20;

    ValueSwap(a, b);  // 值传递,不会改变函数外参数的值,此时 a=10, b=20
    cout << "a:" << a << " b:" << b << endl;  //

    PointerSwap(&a, &b);  // 地址传递,会改变函数外参数的值,此时 a=20, b=10
    cout << "a:" << a << " b:" << b << endl;

    ReferenceSwap(a, b);  // 引用传递,会改变函数外参数的值,此时 a=10, b=20
    cout << "a:" << a << " b:" << b << endl;
}
  • 局部变量的引用和静态变量的引用
// 返回局部变量引用
int &TestFun01() {
    int a = 10;
    return a; // 不要返回局部变量的引用
}

// 返回静态变量引用
int &TestFunc02() {
    static int a = 20;
    cout << "static int a : " << a << endl;
    return a;  // 可以返回静态变量的引用
} // 如果函数的返回值是静态变量的引用,那么这个函数调用可以作为左值
b、引用的本质
  • 引用的本质: Type& ref = val; =========> Type* const ref = &val;,本质是一个指针常量
  • C++ 编译器在编译过程中使用常量指针对其进行替换,此过程对用户不可见
  • 所以引用不可以改变指向,但可以改变内存中的值,占用内存大小与指针相同
// 发现是引用,转换为 int* const ref
void testFunc(int &ref) {
    ref = 100; // ref是引用,转换为 *ref = 100
}

int main() {
    int a = 10;
    int &aRef = a; // 自动转换为 int* const aRef = &a
    aRef = 20;     // 内部发现 aRef 是引用,自动帮我们转换为: *aRef = 20;

    cout << "a:" << a << endl;  // 20
    cout << "aRef:" << aRef << endl;  // 20

    testFunc(a);
    cout << "a:" << a << endl;  // 100
    return 0;
}
c、指针引用和常量引用
  • 指针引用:用一级指针引用可以代替二级指针
  • 常量引用:可以修饰形参为只读,主要用在函数的形参,尤其是类的拷贝/复制构造函数
  • 将函数的形参定义为常量引用的好处:
    • 引用不产生新的变量,减少形参与实参传递时的开销
    • 如果希望实参随着形参的改变而改变,那么使用一般引用,如果不希望实参随着形参改变,那么使用常引用
// 常量引用:防止函数中意外修改数据
void ShowVal(const int& param){
	cout << "param:" << param << endl;
}

二、面向对象的特征及 C++ 的特点

1、面向对象的特征

  • 封装性:
    • 把对象的属性和方法结合成一个独立的系统单位,并尽可能隐藏对象的内部细节,外部若想访问,必须通过指定的函数接口
    • 封装是面向对象思想描述的基础,从此程序员不再面对一个个变量和函数,而是要放眼大局,面对一个个对象来看问题
  • 继承性:
    • 子类自动共享父类之间数据和方法的机制
  • 多态性:
    • 在基类中定义的 属性和方法 被子类继承后,可以具有不同的 数据类型或者表现行为(方法) 等特性
    • 可以对不同类的对象调用相同的方法,产生不同的结果

2、C++ 的特点

  • 封装和信息隐藏;抽象数据类型
  • 继承和派生的方式实现程序的重用机制
  • 通过函数与运算符的重载,派生类中虚函数的多重定义,实现多种情形下的多态特征
  • 通过模板等实现了类型和函数定义的参数化

三、C/C++ 中输入输出接口的使用

1、scanf()/printf() 的使用

  • printf 函数(f: format)

    • 使用形式:printf("控制字符串", 输出列表)
    • 控制字符串:由普通字符、格式字符(eg: %m.nf)、转义字符(eg: \n)这三类内容组成,格式字符:%[flag][width][.precision]type
      • unsigned int、int、float、double、char、char a[]、void*(地址) 型变量对应的格式字符为 %u %d(%x)、%f、%lf、%c、%s、%p or %08X
      • 占位符 %m.nf or %m.nd: m 代表设定的总位数(包括小数点,不够则补空格),n 代表设定小数点后的位数或整数前的位数(不够则补 0)
      • %06.2f:表示位宽总共为 6,小数点也占位,小数点后保留两位有效数字(四舍五入)。eg: 输入 3.493; 输出 003.49
    • 输出列表:可以是常量、变量或表达式,数据之间用逗号","分隔开来(不用括号),数据的个数和类型必须与控制字符串中格式说明的个数和要求一致
  • flag 标志字符:
    在这里插入图片描述

  • width 最小输出宽度:

    • 至少占用几个字符的位置;例如,%-9d 中 width 对应 9,表示输出结果最少占用 9 个字符的宽度。
    • 当输出结果的宽度不足 width 时,以空格补齐(如果没有指定对齐方式,默认会在左边补齐空格);当输出结果的宽度超过 width 时,width 不再起作用,按照数据本身的宽度来输出
  • .precision 输出精度: 小数的位数

    • 当小数部分的位数大于 precision 时,会按照四舍五入的原则丢掉多余的数字;
    • 当小数部分的位数小于 precision 时,会在后面补 0
  • type 输出类型:
    在这里插入图片描述

  • scanf 函数(f: format)

    • 使用形式:scanf("控制字符串", 地址列表)
    • 控制字符串: 由普通字符(要求用户在键盘上输入的字符,一般不要)格式字符(eg: %m.nf)这二类内容组成
    • 地址列表:既可以是变量的地址,也可以是数组名或指针变量名,变量地址的获取方式:&变量
    • 使用方法:
      • 先定义变量, int、float、double、char、char a[]、void* 型变量对应的格式字符为 %d(%x)、%f、%lf、%c、%s、%p,然后再通过 scanf 使用
      • 当用一条 scanf 语句输入多个数据时, 数据的分割符 要和 控制字符串中指定的分割符一致

2、cin/cout 的使用

  • cin (代表键盘) 的使用形式:
    • cin >> 变量1 >> 变量2 >> ... >> 变量n, 变量使用前要提前定义数据类型,当输入多个数据时,用空格、Tab 键或回车键分隔
    • >> 提取操作符,它一次从 输入流对象 cin 提取一个指定类型的数据
    • while(cin >> i) 中,表达式 cin >> i 返回输入流对象本身,也就是 cin;如果到达了文件尾或者提取操作符遇到一个非法值,这个返回值将是 false
  • cout (代表显示器) 的使用形式:
    • cout << 表达式1 << 表达式2 <<' '<< ... << 表达式n << endl,插入了空格和换行符
    • << 为插入操作符,一次插入一个字符串数据到 输出流对象 cout(console out)
      这里写图片描述
  • cin cout 的方法如下:
    #include <iostream>
    using namespace std;  // 显示声明使用 std 命名空间
    
    int main()
    {
    	cin.peek();	 
    	// 从输入流中的字符挑取一个字符,将其提取出来进行其他操作,该操作不会改变输入流中的内容。
    	// 就好比从一堆苹果中拿出来和照片中的苹果比一比再放回去一样
    	
    	cin.get(a);	
    	// 从输入流中获取一个字符到 a 中也可以 a = cin.get(); 
    	// 拓展:cout << cin.get(); 输出的是获取的字符的 ASCII 的值;cout << cin.get(p);输出的是 p 的内存地址
    	
    	cin.ignore(a, ch);  
    	// 方法是从输入流中提取字符,提取的字符被忽略不被使用。
    	// 每抛弃一个字符,它都要计数和比较字符:如果计数值达到 a 或者被抛弃的字符是 ch,则 cin.ignore() 函数执行终止;
    	// 否则,它继续等待。它的一个常用功能就是用来清除以回车结束的输入缓冲区的内容,消除上一次输入对下一次输入的影响;
    	
    	cin.getline(buf, a); 
    	// 方法是从输入流中读取 a 个字符到字符串 buf 中,如果输入不足 a 个字符那么读到换行符为止(适用于输入整行内容)
    	// 拓展:cin.getline()包含三个参数,第三个参数默认为'\0'若,将其改为cin.getline(buf,5,'a')当输入为jkajk时候输出为jk,当输入为jkjkjk时候,输出为jkjk;
    	
    	cin.gcount();
    	// 计算从输入流读入到字符串中字符的个数;
    	
    	cin.read(buf,a);
    	// 从输入流中读取 a 的字符 buf 字符串中;
    	
    	cin.write(buf,a);
    	//  从buf 字符串中读取 a 个字符到输出流中;
    	
    	cout.precision(a);
    	// 设置输出的精确度,保留 a 位小数;
    	
    	cout.width(a);
    	// 设置下一次输出的输出宽度为 a,输出的右对齐,不足的用空格补足,超过的输出全部的数据;
    	
    	return 0;
    }
    

四、生成可执行文件的步骤

在这里插入图片描述

  • gcc/g++ 的用法:gcc/g++ xxx.c [options] file...

    • -o file:指定生成的输出文件名为 file,如果不指定,会自动生成一个默认名 a.out
    • -E:只进行预处理
    • -S(大写):只进行预处理、编译,不进行汇编和链接
    • -c(小写):只进行预处理、编译和汇编,不进行链接,会自动生成一个默认名 ×××.o
    • 其它参数:
      • -shared :指定生成动态链接库
      • -static :指定生成静态链接库
      • -fPIC :创建与地址无关的编译程序(pic,position independent code),是为了能够在多个应用程序间共享
      • -lxxx:指定链接时需要的动态库 xxx,编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上 lib,后面加上.a/.so来确定库的名称
    • 分步编译流程如下图所示:
      在这里插入图片描述
  • ldd 命令:查看一个可执行程序所依赖的动态库

# eg:ldd a.out
linux-vdso.so.1 =>  (0x00007ffe2aaad000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1d651c3000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1d6558d000)
  • nm 命令:查看库中到底有哪些函数
# eg:nm libmain.a
main.o:
                 U exp
0000000000000000 T main    # 库中定义的函数,用 T 表示
                 U printf  # 库中被调用,但并没有在库中定义(表明需要其他库支持),用 U 表示
                 U puts

  • 编译器(gcc/g++/mingw):将易于编写、阅读和维护的高级计算机语言翻译为计算机能解读、运行的低级机器语言的程序

五、C/C++ 预编译与混合编译

5.1 预编译指令

  • 所有以 # 开头的行,都代表 预编译指令,预编译指令行结尾是没有分号的,可以完成宏定义、条件编译、文件包含等

  • #include "文件名"#include <文件名>的区别:前者预处理时首先在当前文件所在的文件目录(文件名中若包含路径,则在指定路径查找头文件)中寻找,若找不到才到系统指定的文件夹中查找;后者直接在系统指定的文件夹中寻找(通常用于包含标准头文件)

  • 宏定义

// 无参宏定义:
#define 标识符 替换列表 
#define N (3+2)
int r=N*N;  // 替换后为 int r=(3+2)*(3+2); 不加括号可能会出现逻辑上的错误

// ANSI C 常用的预定义宏
__LINE__:表示当前源代码的行号
__FILE__:表示当前源文件的名称
__DATE__:表示当前的编译日期
__TIME__:表示当前的编译时间
__cplusplus:当编写 C++ 程序时该标识符被定义

// 带参宏定义:可以不考虑数据的类型(既是优点也是缺点:优点是可用于多种数据类型,缺点是类型不安全)
#define 标识符(参数1,参数2,...,参数n) 替换列表
#define MUL(a,b) ((a)*(b))

// 宏参数的字符串化: # 用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号
#define STR(s) #s
printf("%s", STR(hello));  // 展开为:printf("%s", “hello”);

// 宏参数的连接: ## 称为连接符,用来将宏参数或其他的串连接起来
#define CON1(a, b) a##e##b
#define CON2(a, b) a##b##00

printf("%f\n", CON1(8.5, 2));  // 展开为:printf("%f\n", 8.5e2);
printf("%d\n", CON2(12, 34));  // 展开为:printf("%d\n", 123400);

// Note:
// 宏定义仅是做简单的文本替换,故替换列表中如有表达式,必须把该表达式用括号括起来,否则可能会出现逻辑上的“错误”
// 在宏定义时,除单一值参数外,替换列表中的每个参数均加括号,整个替换列表也加括号
#define MUL(a,b) (a*b)  // 错误示例
int c = MUL(3, 5+1);    // 会替换成 c=(3*5+1)=16; 与预期功能不符

// 简单函数宏定义: 在预处理阶段进行展开,没有函数调用的开销,加快程序运行速度
// C++ 中可以用内联函数 inline void func() 来实现,但内联函数会占用空间,可看作是以空间换时间
#ifndef MIN
#define MIN(a, b)   ((a) > (b) ? (b) : (a))
#endif

#ifndef MAX
#define MAX(a, b)   ((a) > (b) ? (a) : (b))
#endif

#ifndef ABS
#define ABS(x) ((x) >= 0 ? (x) : (-(x)))
#endif


  • 条件编译
#define    // 定义一个预处理宏
#undef     // 取消宏的定义

// 对部分源程序行只在满足一定条件时才编译(即对这部分源程序行指定编译条件)
#if        // 编译预处理中的条件命令,相当于 C 语法中的 if 语句
#else      // 与#if, #ifdef, #ifndef对应, 若这些条件不满足,则执行#else之后的语句,相当于C语法中的else
#endif

/*************作用:可以区隔一些与特定头文件、程序库和其他文件版本有关的代码 *************/
#ifdef     // 判断某个宏是否被定义,若已定义,执行随后的语句
#elif      // 若 #if, #ifdef, #ifndef 或前面的 #elif 条件不满足,则执行 #elif 之后的语句,相当于 C 语法中的 else-if
#endif

#ifndef    // 与 #ifdef 相反,判断某个宏是否未被定义
#else
#endif     // #if, #ifdef, #ifndef 这些条件命令的结束标志


// 防止头文件被重复包含
#ifndef _SOMEFILE_H 
#define _SOMEFILE_H
// 需要声明的变量、函数 
// 宏定义 
// 结构体
#endif



// 当有两个宏定义后执行同一指令时,可使用 defined 来解决
// 在 CMakelists.txt 中使用 add_definitions(-DARCH_ARMV7) 来区分
#if (defined ARCH_ARMV7) || (defined ARCH_ARMV8)
    printf("ARCH_ARMV7 or ARCH_ARMV8 is defined\n");
#endif


// X86平台32/64位Linux系统
#if defined (ARCH_X86_32_LINUX) || defined (ARCH_X86_64_LINUX)
    typedef long long          s64;
    typedef unsigned long long u64;
	
	#define INLINE inline
	#define FAST_CALL __attribute__((fastcall))
	#define EXPORT 
	#define RESTRICT 
	
// X86平台32/64位Windows系统
#elif defined (ARCH_X86_32_WIN) || defined (ARCH_X86_64_WIN)
	typedef __int64            s64;
    typedef unsigned __int64   u64;
    
	#define INLINE __inline
	#define FAST_CALL __fastcall
	#define EXPORT __declspec(dllexport)
	#define RESTRICT 

// ARM平台32位Linux系统
#elif defined ARCH_ARM_X86_LINUX
    typedef long long          s64;
    typedef unsigned long long u64;
	
	#define INLINE inline
	#define FAST_CALL __attribute__((fastcall))
	#define EXPORT 
	#define RESTRICT 
	
// ARM平台64位Linux系统
#elif defined ARCH_ARM_X64_LINUX
    typedef long               s64;
    typedef unsigned long      u64;
	
	#define INLINE inline
	#define FAST_CALL __attribute__((fastcall))
	#define EXPORT 
	#define RESTRICT 
	
#endif

5.2 C/C++ 混合编译

  • 一个项目中 .cpp 调用的头文件中含有需要 gcc 编译的部分,那么需要使用 extern “C”{} 让这段代码按照 C 语言的方式进行编译、链接
  • 使用 extern “C”{}的主要原因是:
    • C++ 对函数进行了重载,在编译生成的汇编码中会对函数的名字进行一些处理,加入比如函数的参数类型、个数、顺序等,而在 C 中,只是简单的函数名字而已,不会加入其它的信息
    • 若在 C++ 中调用一个使用 C 语言编写的函数,C++ 会根据C++名称修饰方式来查找并链接这个函数,那么就会发生 链接错误
    // 1、C 和 C++ 中对同一个函数经过编译后生成的函数名是不相同的
    C 函数: void MyFunc(){}, 被编译成函数: MyFunc
    C++ 函数: void MyFunc(){}, 被编译成函数: _Z6Myfuncv  
    // C++ 中调用 MyFunc 函数,在链接阶段会去找 _Z6Myfuncv,
    // 结果是没有找到的,因为这个 MyFunc 函数是 C 语言编写的,编译生成的符号是 MyFunc
    
  • 代码实现示例:
// 1、在相应的 .h 头文件中包含如下代码
#ifdef __cplusplus  // __cplusplus 是 g++ 编译器中的自定义宏,用于说明正在使用 g++ 编译
extern "C" {
#endif

// 一段声明代码,使用 gcc 来编译

#ifdef __cplusplus
}
#endif

// 2、直接在 cpp 使用 extern "C" 包含 C 头文件
extern "C" {
// 一段声明代码,使用 gcc 来编译
}

六、C/C++ 精进

  • 《C++ Primer》 :被誉为 C++ 的圣经,非常适合初学者作为 C++ 的工具书和复习教材
  • 《C++编程思想》:侧重介绍 C++ 的编程思想和设计模式,适合有一定编程基础后进一步提高的学习者
  • 《Effective C++》:主要介绍 C++ 编程中的 55 个经验准则,如何写出简洁高效的 C++ 代码。适合已有一定 C++ 基础的程序员读
  • 《STL源码剖析》:分析探讨 C++ 标准模板库的实现原理,适合想深入理解 STL 的进阶者

六、参考资料

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值