预处理指令和强制转换

以上内容总结至:微软官网https://docs.microsoft.com/zh-cn/cpp/preprocessor/preprocessor-directives?view=msvc-170

预处理器指令:(如 #define 和 #ifdef )通常用于使源程序在不同的执行环境中易于更改和编译预处理器语句使用的字符集与源文件语句相同,但转义序列不受支持。 预处理器语句中使用的字符集与执行字符集相同。 预处理器还可识别负字符值。

常见的预处理关键词:

  •  (#) 必须是包含指令的行上的第一个非空白字符
  • 指令后面的任何文本都必须以单行注释分隔符开头 (//) 或括在注释分隔符 (/* */)
  • 预处理器指令的行可以通过紧靠在行尾标记前面,使用反斜杠 (\

#define :

创建一个宏,该是标识符或参数化标识符与标记字符串的关联。 在定义宏之后,编译器可用标记字符串替换源文件中标识符的每个匹配项

#define  I   30  //把 I 定义为整形常量

//这两种的区别
#define  P (x+y)
#define  P(x,y)  x+y

int x=20;int y=30
求 k=x*P(x,y)*y
//第一种
k=x*(x+y)*y=20*(20+30)*30=30000
//第二种
k=x*x+y*y=20*20+30*30=1300

#undef :

移除(取消定义)之前使用 #define 创建的名称

#define  P (x+y) //创建
#define  P1  x+y //创建
...
...
#undef P  //删除
#undef P1 //删除

 #error:

在编译时发出用户指定的错误消息,然后终止编译

#if、#elif、#else 和 #endif  :

控制源文件部分编译:

#if 每个命令指令都必须由结束的 #endif 指令匹配 。 在 #elif 和 #if 指令之间#endif任意数目的 #else 指令,但最多允许一#else指令。 如果存在 #else ,则此指令必须是最后一个指令,然后才能 #endif#if、#elif#else 和 #endif 指令可以嵌套在其他 #if 指令的文本部分中。 每个 嵌套#else、 #elif 或 #endif 指令都属于 最近的前一 个#if指令。

格式:

#if ......

#elif......//可以没有这一行

#else.....

#endif.... 

defined:预处理运算符 

用法:

已 (标识符)
定义标识符

#if defined(CREDIT)
    credit();
#elif defined(DEBIT)
    debit();
#else
    printerror();
#endif
//如果定义了 CREDIT ,则编译对credit()调用
//如果定义了标识符 DEBIT,则编译对 debit 的函数调用。 
//如果两个标识符都未定义,则编译对 printerror 的调用。
假设 p是一个已定义的量
#if    p>5
     #define G  1
#elif  p=5
     #define G  2
#else 
     #define G  3
#endif
//当p大于5时  G 为 1
//当p等于5时  G 为 2
//两个都不满足时  G 为 1

#ifdef 和#ifndef 

相当于:#if  defined ...     #if !defined ...

提供这些指令只是为了实现与该语言的早期版本的兼容性

首选 defined(identifier) 与 指令一起使用的 #if 常量表达式。

#import  :(C++专用)

过去一直合并类型库中的信息。 类型库的内容将转换为 C++ 类,主要描述 COM 接口

#import "filename" [attributes]
<#import文件名>[属性]

#include :头文件

告知预处理器在 指令出现时包含指定文件的内容。

#include "path-spec" //用户自定义的文件
#include <path-spec>//系统文件

#include<iostream>//系统文件
#include"Person.h"//用户自定义文件

这两个包含的区别: 

  1. #include <> 编译器直接从系统类库目录里查找头文件,如果类库目录下查找失败,编译器会终止查找。
  2. #include "" 编译器默认从当前文件所在目录下查找头文件,如果查找失败,再从项目工程中设置的头文件引用目录查找,在 Linux GCC 编译环境下,则一般通过使用 -L 参数指定引用目录,如果项目配置的头文件引用目录中仍然查找失败,再从系统类库目录里查找头文件。
  3. #include <> 一般用于包含系统头文件,诸如 stdlib.h、stdio.h、iostream 等;#include "" 一般用于包含自定义头文件,比如我们自定义的 test.h、declare.h 等。

C++ 中的预定义宏

C++ 提供了下表所示的一些预定义宏:

描述
LINE这会在程序编译时包含当前行号。
FILE这会在程序编译时包含当前文件名。
DATE这会包含一个形式为 month/day/year 的字符串,它表示把源文件转换为目标代码的日期。
TIME这会包含一个形式为 hour:minute:second 的字符串,它表示程序被编译的时间。
#include <iostream>
using namespace std;

int main ()
{
    cout << " __LINE__ : " << __LINE__ << endl;
    cout << "__FILE__ : " << __FILE__ << endl;
    cout << " __DATE__ : " << __DATE__ << endl;
    cout << " __TIME__ : " << __TIME__ << endl;

    return 0;
}
结果为:
__LINE__ : 6   //行号
__FILE__ : test.cpp //文件
__DATE__ : Feb 28 2011 //日期
__TIME__ : 18:52:48 //时间

预处理器运算符:

操作员操作
字符串化运算符 (#)使相应的实际参数用双引号引起来
字符化运算符 (#@)使相应的参数括在单引号中,并被视为特定于 Microsoft (字符)
标记粘贴运算符 (##)允许连接用作实际参数的令牌以形成其他标记
defined 运算符简化了在某些宏指令中编写复合表达式

运算符用于连接两个令牌:

#define CONCAT( x, y )  x ## y

#include <iostream>
using namespace std;
 
#define concat(a, b) a ## b
int main()
{
   int xy = 100;
 
   cout << concat(x, y);  //结果为100  concat 把(x,y)拼接成 xy
   return 0;
}

宏和 C++ :

  • 在 C++ 中,声明为 const 的对象可用于常量表达式。 它允许程序声明具有类型和值信息的常量。 它们可以声明可以使用调试器以符号方式查看的枚举。
  • 使用预处理器指令 #define 定义常量时,它并不精确,也不为类型安全。 不会为 对象分配任何存储 const ,除非程序包含采用其地址的表达式。
  • C++ 内联函数功能取代了函数类型宏。 使用内联函数取代宏的好处如下:

    • 类型安全。 内联函数需要接受与常规函数相同的类型检查。 宏不是类型安全的

    • 纠正具有副作用的参数的处理。 内联函数在输入函数体之前计算作为参数提供的表达式。 因此,具有副作用的表达式不会不安全。

四大强制类型转换:

参考资料C++ 四种强制类型转换 - 静悟生慧 - 博客园 (cnblogs.com)

C++ static_cast、dynamic_cast、const_cast和reinterpret_cast(四种类型转换运算符) (biancheng.net)

static_cast (静态类型转换)

主要用于普通对象,static_cast没有动态类型检查,所以是不安全的,在编译期间转换

常见的用法:

  1. 向上转型(类和普通变量都可以)
  2. 向下转型(普通变量),类向下转型会报错
  3. void *转换为各类指针(其他指针转换为void无需强制转换)

 注意:static_cast不能转换掉expression的const、volitale或者__unaligned属性。

向上向下转型 

class person
{

};
class son:public person
{

};
int main()
{
	int a = 10;
	double b = static_cast<double>(a);//int -> double 普通变量向上转型
	int a1 = static_cast<int>(b);//普通变量可以向下转型
	son s;
	person p = static_cast<person>(s);//son -> person 类向上转型
	son s1 = static_cast<son>(p);//报错无法强制转换(类向下转换)
	return 0;
}

各类指针和void *的转换

class person
{

};
int main()
{
	int a = 10;
	int *p = &a;
	void *v = p;//其他指针转void *可以不用强制类型转换
	int *p1 = v;//报错无法转换,需要用强制转换
	int *p2 = static_cast<int *>(v);
	person per;
	person* per1= &per;
	void *son = per1;//类的指针可以转换为void *
	person *per2 = static_cast<person*>(son);
	return 0;
}

const_cast(常量类型转换)

从const转换为非const,从volatile转换为非volatile(并不是真正的去除const),一般不会使用

注意事项:这两种初始化的区别

  1. const  int  a=10;
  2. int p=10; const  int a=p;
void text() {
	const int a = 10;
	int *p;
	p = const_cast<int*>(&a);
	(*p)++;
	cout << a << endl;//结果为10
	cout << *p << endl;//结果为11
	cout << "-----------" << endl;
}
void text1() {
	int i = 10;
	const int a = i;
	int *r = const_cast<int *>(&a);
	(*r)++;
	cout << i << endl;//结果为10
	cout << a << endl;//结果为11
	cout << *r << endl;//结果为11
}
int main() {
	text();
	text1();
	return 0;
}

 解释:

const  int  a=10;时,编译器会进行优化,直接将10代替a,所以a是不可以改变的

int p=10; const  int a=p;时,编译器不会优化,不会直接用p代替a,所以当运行(*r)++;会改变a中的值

reinterpret_cast(重解释转换)

是一种不安全的转换,用于指针和引用,是一种位操作。

主要用途:

  1. 改变指针或引用的类型
  2. 将指针或引用转换为一个足够长度的整形
  3. 将整型转换为指针或引用类型

void text1() {
	int *m = new int(10);
	double *n = reinterpret_cast<double*>(m);//指针和指针转换

	int * a = new int(5);
	int b = reinterpret_cast<int>(a);//指针转化为整型

	int c = 10;
	int *d = reinterpret_cast<int *>(c);//整型转化为指针
}
int main() {
	text1();
	return 0;
}

 以上三种都是编译时进行类型转换

dynamic_cast(动态转换)

dynamic_cast的注意事项:

  1. dynamic_cast是运行的时候借助 RTTI 进行类型转换(必须包含虚函数
  2. dynamic_cast转换如果成功的话返回的是指向类的指针或引用,转换失败的话则会返回NULL
  3. dynamic_cast作用与类的指针和引用或void*,不能用于常规类型强制转换 

不包含虚函数使用不了(动态多态)

  1.  向上转型:条件  (有继承关系,有虚函数)向上转型的话一般是安全的,不会进行任何运行期间的检查,这时相当于static_cast
  2. 向下转型:运行的时候借助 RTTI 进行类型转换,向下转换的成功与否还与将要转换的类型有关,即要转换的指针指向的对象的实际类型与转换以后的对象类型一定要相同,否则转换失败(向下转型看本质)

向上转型: 

class person{
public:
	virtual void show() { cout << "person类" << endl; }
};
class son:public person
{
public:
	void show() { cout << "son类" << endl; }
};
void text1() {
	//向上转型,static_cast和dynamic_cast等价
	son *s = new son;
	person * p = dynamic_cast<person *>(s);

	son *s1 = new son;
	person * p1 = static_cast<person *>(s1);	
}
int main() {
	text1();
	return 0;
}

向下转型:

注意:向下转型其实也是(向上转型)

例一:失败的案例

class person{
public:
	virtual void show() { cout << "person类" << endl; }
};
class son:public person
{
public:
	virtual void show() { cout << "son类" << endl; }
};
class man :public son
{
public:
	virtual void show() { cout << "man类" << endl; }
};
int main() {
	person *p = new person;
	son *s = dynamic_cast<son*>(p);
	if (s == NULL)
	{
		cout << "s转换失败" << endl;
	}
	man *m = dynamic_cast<man*>(p);
	if (m == NULL)
	{
		cout << "m转换失败" << endl;
	}
	return 0;
}

转化失败的原因: dynamic_cast 会沿着继承链寻找想要转化的目标,没有找到返回null

例二:转化成功的案例

int main() {
	person *p = new man;
	son *s = dynamic_cast<son*>(p);
	if (s != NULL)
	{
		cout << "s转换成功" << endl;
	}
	man *m = dynamic_cast<man*>(p);
	if (m != NULL)
	{
		cout << "m转换成功" << endl;
	}
	return 0;
}

为PDF文件增加了目录,便于快速浏览。 摘录一些不知道以及没有做到的: 1、防止多重包含(头文件的形式不能完美做到) 所有头文件都应该使用#define防止头文件被多重包含(multiple inclusion),命名格式当是:<PROJECT>_<PATH>_<FILE>_H_ 为保证唯一性,头文件的命名应基于其所在项目源代码树的全路径。例如,项目foo中的头文件foo/src/bar/baz.h按如下方式保护: #ifndef FOO_BAR_BAZ_H_ #define FOO_BAR_BAZ_H_ ... #endif // FOO_BAR_BAZ_H_ 2、能依赖声明就不要依赖定义 使用前置声明(forward declarations)尽量减少.h文件中#include的数量。 当一个头文件被包含的同时也引入了一项新的依赖(dependency),只要该头文件被修改,代码就要重新编译。如果你的头文件包含了其他头文件,这些头文件的任何改变也将导致那些包含了你的头文件的代码重新编译。因此,我们宁可尽量少包含头文件,尤其是那些包含在其他头文件中的。 使用前置声明可以显著减少需要包含的头文件数量。举例说明:头文件中用到类File,但不需要访问File的声明,则头文件中只需前置声明class File;无需#include "file/base/file.h"。 在头文件如何做到使用类Foo而无需访问类的定义? 1) 将数据成员类型声明为Foo *或Foo &; 2) 参数、返回值类型为Foo的函数只是声明(但不定义实现); 3) 静态数据成员的类型可以被声明为Foo,因为静态数据成员的定义在类定义之外。 另一方面,如果你的类是Foo的子类,或者含有类型为Foo的非静态数据成员,则必须为之包含头文件。 3、为什么要使用内联函数,又如何改进性能? 定义(Definition):当函数被声明为内联函数之后,编译器可能会将其内联展开,无需按通常的函数调用机制调用内联函数。 重要的是,虚函数和递归函数即使被声明为内联的也不一定就是内联函数。通常,递归函数不应该被声明为内联的(译者注:递归调用堆栈的展开并不像循环那么简单,比如递归层数在编译时可能是未知的,大多数编译器都不支持内联递归函数)。析构函数内联的主要原因是其定义在类的定义中,为了方便抑或是对其行为给出文档。 4. 函数参数顺序(Function Parameter Ordering) 定义函数时,参数顺序为:输入参数在前,输出参数在后。 C/C++ 函数参数分为输入参数和输出参数两种,有时输入参数也会输出(译者注:值被修改时)。输入参数一般传值或常数引用(const references),输出参数或输入/输出参数为非常数指针(non-const pointers)。对参数排序时,将所有输入参数置于输出参数之前。不要仅仅因为是新添加的参数,就将其置于最后,而应该依然置于输出参数之前。 5、头文件定义顺序 将包含次序标准化可增强可读性、避免隐藏依赖(hidden dependencies,译者注:隐藏依赖主要是指包含的文件中编译时),次序如下:对应该CPP的头文件、C库、C++库、其他库的.h、项目内的.h。 相同目录下头文件按字母序是不错的选择。 1. 命名空间(Namespaces) 在.cc文件中,提倡使用不具名的命名空间(unnamed namespaces,译者注:不具名的命名空间就像不具名的类一样,似乎被介绍的很少:-()。使用具名命名空间时,其名称可基于项目或路径名称,不要使用using指示符。 定义:命名空间将全局作用域细分为不同的、具名的作用域,可有效防止全局作用域的命名冲突。 优点:命名空间提供了(可嵌套)命名轴线(name axis,译者注:将命名分割在不同命名空间内),当然,类也提供了(可嵌套)的命名轴线(译者注:将命名分割在不同类的作用域内)。 举例来说,两个不同项目的全局作用域都有一个类Foo,这样在编译或运行时造成冲突。如果每个项目将代码置于不同命名空间中,project1::Foo和project2::Foo作为不同符号自然不会冲突。 缺点:命名空间具有迷惑性,因为它们和类一样提供了额外的(可嵌套的)命名轴线。在头文件中使用不具名的空间容易违背C++的唯一定义原则(One Definition Rule (ODR))。 1) 不具名命名空间(Unnamed Namespaces) 在.cc文件中,允许甚至提倡使用不具名命名空间,以避免运行时的命名冲突: namespace { // .cc 文件中 // 命名空间的内容无需缩进 enum { UNUSED, EOF, ERROR }; // 经常使用的符号 bool AtEof() { return pos_ == EOF; } // 使用本命名空间内的符号EOF } // namespace 然而,与特定类关联的文件作用域声明在该类中被声明为类型、静态数据成员或静态成员函数(什么意思?),而不是不具名命名空间的成员。像上文展示的那样,不具名命名空间结束时用注释// namespace标识。 2、函数重载 结论:如果你想重载一个函数,考虑让函数名包含参数信息,例如,使用AppendString()、AppendInt()而不是Append()。 3. 缺省参数(Default Arguments) 禁止使用缺省函数参数。 优点:经常用到一个函数带有大量缺省值,偶尔会重写一下这些值,缺省参数为很少涉及的例外情况提供了少定义一些函数的方便。 缺点:大家经常会通过查看现有代码确定如何使用API,缺省参数使得复制粘贴以前的代码难以呈现所有参数,当缺省参数不适用于新代码时可能导致重大问题。 结论:所有参数必须明确指定,强制程序员考虑API和传入的各参数值,避免使用可能不为程序员所知的缺省参数。 8. 类型转换(Casting) 使用static_cast<>()等C++的类型转换,不要使用int y = (int)x或int y = int(x);。 定义:C++引入了有别于C的不同类型的类型转换操作。 优点:C语言的类型转换问题在于操作比较含糊:有时是在做强制转换(如(int)3.5),有时是在做类型转换(如(int)"hello")。另外,C++的类型转换查找更容易、更醒目。 缺点:语法比较恶心(nasty)。 结论:使用C++风格而不要使用C风格类型转换。 1) static_cast:和C风格转换相似可做值的强制转换,或指针的父类到子类的明确的向上转换; 2) const_cast:移除const属性; 3) reinterpret_cast:指针类型和整型或其他指针间不安全的相互转换,仅在你对所做一切了然于心时使用; 4) dynamic_cast:除测试外不要使用,除单元测试外,如果你需要在运行时确定类型信息,说明设计有缺陷(参考RTTI)。 10. 前置自增和自减(Preincrement and Predecrement) 对于迭代器和其他模板对象使用前缀形式(++i)的自增、自减运算符。 定义:对于变量在自增(++i或i++)或自减(--i或i--)后表达式的值又没有没用到的情况下,需要确定到底是使用前置还是后置的自增自减。 优点:不考虑返回值的话,前置自增(++i)通常要比后置自增(i++)效率更高,因为后置的自增自减需要对表达式的值i进行一次拷贝,如果i是迭代器或其他非数值类型,拷贝的代价是比较大的。既然两种自增方式动作一样(译者注,不考虑表达式的值,相信你知道我在说什么),为什么不直接使用前置自增呢? 缺点:C语言中,当表达式的值没有使用时,传统的做法是使用后置自增,特别是在for循环中,有些人觉得后置自增更加易懂,因为这很像自然语言,主语(i)在谓语动词(++)前。 结论:对简单数值(非对象)来说,两种都无所谓,对迭代器和模板类型来说,要使用前置自增(自减)。 16. sizeof(sizeof) 尽可能用sizeof(varname)代替sizeof(type)。 使用sizeof(varname)是因为当变量类型改变时代码自动同步,有些情况下sizeof(type)或许有意义,还是要尽量避免,如果变量类型改变的话不能同步。 8. TODO注释(TODO Comments) 对那些临时的、短期的解决方案,或已经够好但并不完美的代码使用TODO注释。 这样的注释要使用全大写的字符串TODO,后面括号(parentheses)里加上你的大名、邮件地址等,还可以加上冒号(colon):目的是可以根据统一的TODO格式进行查找: // TODO(kl@gmail.com): Use a "*" here for concatenation operator. // TODO(Zeke) change this to use relations. 如果加上是为了在“将来某一天做某事”,可以加上一个特定的时间("Fix by November 2005")或事件("Remove this code when all clients can handle XML responses.")。 TODO很不错,有时候,注释确实是为了标记一些未完成的或完成的不尽如人意的地方,这样一搜索,就知道还有哪些活要干,日志都省了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值