文章目录
一、C 和 C++的区别
- C 是
面向过程
的编程语言,C++ 是面向对象
的编程语言(相比 C 添加了类和模版
等),支持泛型编程 - C 语言中用来做控制输入输出的是
stdio
函数库,而 C++ 中为iostream
- C语言中输入输出使用
scanf()
和printf()
,而 C++ 中为cin
和cout
- C 语言中的换行符为
\n
,而 C++ 中的换行符为endl
- C 语言中使用结构体时需要加上前缀
struct
, 而在 C++ 中则不用 - C 语言中的
malloc
和free
对内存进行分配,C++ 中仍然支持,同时增加了new
和delete
来管理内存(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(1, sizeof(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
默认为外部连接
,当两个文件中有同名只读变量时,编译器会报重定义的错误
- C 中的
// 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
关键字则用于指明其后是一个常量(或者常量表达式),编译器在编译程序时可以顺带将其结果计算出来,而无需等到程序运行阶段,这样的优化极大地提高了程序的执行效率
- 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; // 报错,不能通过变量直接修改 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、#ifdef __cplusplus extern C{}与C和C++间的关系
- 2、#ifdef __cplusplus 到底是什么意思?
- 3、https://en.cppreference.com/w/
- 4、https://cppinsights.io/:把普通语法转换成核心语法
- 5、https://godbolt.org/:显示相应的汇编语言
- 6、https://cpp.sh/:在线编译运行 cpp
- 7、https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines
- 8、C/C++学习+面试指南
- 9、2022_最新C++开发学习路线_科班版
- 10、https://github.com/Light-City/CPlusPlusThings
- 11、https://github.com/changkun/modern-cpp-tutorial
- 12、https://www.runoob.com/cplusplus/cpp-tutorial.html
- 13、https://cplusplus.com/