160、将模板类用作参数
示例:
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
template <class T, int len>
class LinkList // 链表类模板。
{
public:
T* m_head; // 链表头结点。
int m_len = len; // 表长。
void insert() { cout << "向链表中插入了一条记录。\n"; }
void ddelete() { cout << "向链表中删除了一条记录。\n"; }
void update() { cout << "向链表中更新了一条记录。\n"; }
};
template <class T, int len>
class Array // 数组类模板。
{
public:
T* m_data; // 数组指针。
int m_len = len; // 表长。
void insert() { cout << "向数组中插入了一条记录。\n"; }
void ddelete() { cout << "向数组中删除了一条记录。\n"; }
void update() { cout << "向数组中更新了一条记录。\n"; }
};
// 线性表模板类:tabletype-线性表类型,datatype-线性表的数据类型。
template<template<class, int >class tabletype, class datatype, int len>
class LinearList
{
public:
tabletype<datatype, len> m_table; // 创建线性表对象。
void insert() { m_table.insert(); } // 线性表插入操作。
void ddelete() { m_table.ddelete(); } // 线性表删除操作。
void update() { m_table.update(); } // 线性表更新操作。
void oper() // 按业务要求操作线性表。
{
cout << "len=" << m_table.m_len << endl;
m_table.insert();
m_table.update();
}
};
int main()
{
// 创建线性表对象,容器类型为链表,链表的数据类型为 int,表长为 20。
LinearList<LinkList, int, 20> a;
a.insert();
a.ddelete();
a.update();
// 创建线性表对象,容器类型为数组,数组的数据类型为 string,表长为 20。
LinearList<Array, string, 20> b;
b.insert();
b.ddelete();
b.update();
}
165、编译预处理
C++程序编译的过程:预处理 -> 编译(优化、汇编)-> 链接
预处理指令主要有以下三种:
包含头文件:#include
宏定义:#define(定义宏)、#undef(删除宏)。
条件编译:#ifdef、#ifndef。
1)包含头文件
#include 包含头文件有两种方式:
#include <文件名>:直接从编译器自带的函数库目录中寻找文件。
#include "文件名":先从自定义的目录中寻找文件,如果找不到,再从编译器自带的函数库目录
中寻找。
#include 也包含其它的文件,如:*.h、*.cpp 或其它的文件。
C++98 标准后的头文件:
C 的标准库:老版本的有.h 后缀;新版本没有.h 的后缀,增加了字符 c 的前缀。例如:老版本是
<stdio.h>,新版本是<cstdio>,新老版本库中的内容是一样的。在程序中,不指定 std 命名空
间也能使用库中的内容。
C++的标准库:老版本的有.h 后缀;新版本没有.h 的后缀。例如:老版本是<iostream.h>,新
版本是<iostream>,老版本已弃用,只能用新版本。在程序中,必须指定 std 命名空间才能使
用库中的内容。
注意:用户自定义的头文件还是用.h 为后缀。
2)宏定义指令
无参数的宏:#define 宏名 宏内容
有参数的宏:#define MAX(x,y) ((x)>(y) ? (x) : (y)) MAX(3,5) ((3)>(5) ? (3) : (5))
编译的时候,编译器把程序中的宏名用宏内容替换,是为宏展开(宏替换)。
宏可以只有宏名,没有宏内容。
在 C++中,内联函数可代替有参数的宏,效果更好。
C++中常用的宏:
当前源代码文件名:FILE 当前源代码函数名:FUNCTION 当前源代码行号:LINE 编译的日期:DATE 编译的时间:TIME 编译的时间戳:TIMESTAMP 当用 C++编译程序时,宏__cplusplus 就会被定义。
3)条件编译
最常用的两种:#ifdef、#ifndef if #define if not #define
#ifdef 宏名
程序段一
#else
程序段二
#endif
含义:如果#ifdef 后面的宏名已存在,则使用程序段一,否则使用程序段二。
#ifndef 宏名
程序段一
#else
程序段二
#endif
含义:如果#ifndef 后面的宏名不存在,则使用程序段一,否则使用序段二。
4)解决头文件中代码重复包含的问题
在 C/C++中,在使用预编译指令#include 的时候,为了防止头文件被重复包含,有两种方式。
第一种:用#ifndef 指令。
#ifndef GIRL #define GIRL
//代码内容。
#endif
第二种:把#pragma once 指令放在文件的开头。
#ifndef 方式受 C/C++语言标准的支持,不受编译器的任何限制;而#pragma once 方式有些编译
器不支持。
#ifndef 可以针对文件中的部分代码;而#pragma once 只能针对整个文件。
#ifndef 更加灵活,兼容性好;#pragma once 操作简单,效率高。
166、编译和链接
一、源代码的组织
头文件(*.h):#include 头文件、函数的声明、结构体的声明、类的声明、模板的声明、内联函数、
#define 和 const 定义的常量等。
源文件(*.cpp):函数的定义、类的定义、模板具体化的定义。
主程序(main 函数所在的程序):主程序负责实现框架和核心流程,把需要用到的头文件用#include
包含进来。 二、编译预处理
预处理的包括以下方面:
1)处理#include 头文件包含指令。
2)处理#ifdef #else #endif、#ifndef #else #endif 条件编译指令。
3)处理#define 宏定义。
4)为代码添加行号、文件名和函数名。
5)删除注释。
6)保留部分#pragma 编译指令(编译的时候会用到)。 三、编译
将预处理生成的文件,经过词法分析、语法分析、语义分析以及优化和汇编后,编译成若干个目标文
件(二进制文件)。 四、链接
将编译后的目标文件,以及它们所需要的库文件链接在一起,形成一个体整。 五、更多细节
1)分开编译的好处:每次只编译修改过的源文件,然后再链接,效率最高。
2)编译单个*.cpp 文件的时候,必须要让编译器知道名称的存在,否则会出现找不到标识符的错误。
(直接和间接包含头文件都可以)
3)编译单个*.cpp 文件的时候,编译器只需要知道名称的存在,不会把它们的定义一起编译。
4)如果函数和类的定义不存在,编译不会报错,但链接会出现无法解析的外部命令。
5)链接的时候,变量、函数和类的定义只能有一个,否则会出现重定义的错误。(如果把变量、函
数和类的定义放在*.h 文件中,*.h 会被多次包含,链接前可能存在多个副本;如果放在*.cpp 文件中,*. cpp 文件不会被包含,只会被编译一次,链接前只存在一个版本)
6)把变量、函数和类的定义放在*.h 中是不规范的做法,如果*.h 被多个*.cpp 包含,会出现重定义。
7)用#include 包含*.cpp 也是不规范的做法,原理同上。
8)尽可能不使用全局变量,如果一定要用,要在*.h 文件中声明(需要加 extern 关键字),在*.cp
p 文件中定义。
9)全局的 const 常量在头文件中定义(const 常量仅在单个文件内有效)。
10)*.h 文件重复包含的处理方法只对单个的*.cpp 文件有效,不是整个项目。
11)函数模板和类模板的声明和定义可以分开书写,但它们的定义并不是真实的定义,只能放在*.h
文件中;函数模板和类模板的具体化版本的代码是真实的定义,所以放在*.cpp 文件中。
12)Linux 下 C++编译和链接的原理与 VS 一样。
167、命名空间
在实际开发中,较大型的项目会使用大量的全局名字,如类、函数、模板、变量等,很容易出现名字
冲突的情况。
命名空间分割了全局空间,每个命名空间是一个作用域,防止名字冲突。 一、语法
创建命名空间:
namespace 命名空间的名字
{
// 类、函数、模板、变量的声明和定义。
}
创建命名空间的别名:
namespace 别名=原名;
二、使用命名空间
在同一命名空间内的名字可以直接访问,该命名空间之外的代码则必须明确指出命名空间。
1)运算符::
语法:命名空间::名字
简单明了,且不会造成任何冲突,但使用起来比较繁琐。
2)using 声明
语法:using 命名空间::名字
用 using 声明名后,就可以进行直接使用名称。
如果该声明区域有相同的名字,则会报错。
3)using 编译指令
语法:using namespace 命名空间
using 编译指令将使整个命名空间中的名字可用。如果声明区域有相同的名字,局部版本将隐藏命名
空间中的名字,不过,可以使用域名解析符使用命名空间中的名称。 四、注意事项
1)命名空间是全局的,可以分布在多个文件中。
2)命名空间可以嵌套。
3)在命名空间中声明全局变量,而不是使用外部全局变量和静态变量。
4)对于 using 声明,首选将其作用域设置为局部而不是全局。
5)不要在头文件中使用 using 编译指令,如果非要使用,应将它放在所有的#include 之后。
6)匿名的命名空间,从创建的位置到文件结束有效。
示例:
// demo01.cpp ///
#include <iostream> // 包含头文件。
#include "public1.h" #include "public2.h" using namespace std; // 指定缺省的命名空间。
int main()
{
using namespace aa;
using namespace bb;
using bb::ab;
cout << "aa::ab=" << aa::ab << endl;
aa::func1();
aa::A1 a;
a.show();
cout << "bb::ab=" << bb::ab << endl;
}
///
// public2.cpp ///
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
#include "public2.h" namespace aa
{
int ab = 1; // 全局变量。
}
namespace bb
{
int ab = 2; // 全局变量。
void func1() { // 全局函数的定义。
cout << "调用了 bb::func1()函数。\n";
}
void A1::show() { // 类成员函数的类外实现。
cout << "调用了 bb::A1::show()函数。\n";
}
}
///
// public1.cpp ///
#include <iostream> // 包含头文件。
using namespace std; // 指定缺省的命名空间。
#include "public1.h" namespace aa
{
void func1() { // 全局函数的定义。
cout << "调用了 aa::func1()函数。\n";
}
void A1::show() { // 类成员函数的类外实现。
cout << "调用了 aa::A1::show()函数。\n";
}
}
///
// public2.h ///
#pragma once
namespace aa
{
extern int ab; // 全局变量。
}
namespace bb
{
extern int ab ; // 全局变量。
void func1(); // 全局函数的声明。
class A1 // 类。
{
public:
void show(); // 类的成员函数。
};
}
///
// public1.h ///
#pragma once
namespace aa
{
void func1(); // 全局函数的声明。
class A1 // 类。
{
public:
void show(); // 类的成员函数。
};
}
///
168、C++强制类型转换
C 风格的强制类型转换很容易理解,不管什么类型都可以直接进行转换,使用格式如下:
目标类型 b = (目标类型) a;
C++也是支持 C 风格的强制类型转换,但是 C 风格的强制类型转换可能会带来一些隐患,出现一些
难以察觉的问题,所以 C++又推出了四种新的强制类型转换来替代 C 风格的强制类型转换,降低使用风
险。
在 C++中,新增了四个关键字 static_cast、const_cast、reinterpret_cast 和 dynamic_cast,用于
支持 C++风格的强制类型转换。
C++风格的强制类型转换能更清晰的表明它们要干什么,程序员只要看一眼这样的代码,立即能知道
强制转换的目的,并且,在多态场景也只能使用 C++风格的强制类型转换。
一、static_cast
static_cast 是最常用的 C++风格的强制类型转换,主要是为了执行那些较为合理的强制类型转换,
使用格式如下:
static_cast<目标类型>(表达式);
1)用于基本内置数据类型之间的转换
C 风格:编译器可能会提示警告信息。
static_cast:不会提示警告信息。
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
char cc = 'X';
float ff = cc; // 隐式转换,不会告警。
float ffc = static_cast<float>(cc); // 显式地使用 static_cast 进行强制类型转换,不会告警。
double dd = 3.38;
long ll = dd; // 隐式转换,会告警。
long llc = static_cast<long>(dd); // 显式地使用 static_cast 进行强制类型转换,不会告警。
}
2)用于指针之间的转换
C 风格:可用于各种类型指针之间的转换。
static_cast:各种类型指针之间的不允许转换,必须借助 void*类型作为中间介质。
#include <iostream>
int main(int argc, char* argv[])
{
int type_int = 10;
float* float_ptr1 = (float *) & type_int; // int* -> float* 隐式转换无效
// float* float_ptr2 = static_cast<float*>(&type_int); // int* -> float* 使用 static_cast 转换
无效
char* char_ptr1 = (char *) & type_int; // int* -> char* 隐式转换无效
// char* char_ptr2 = static_cast<char*>(&type_int); // int* -> char* 使用 static_cast 转换
无效
void* void_ptr = &type_int; // 任何指针都可以隐式转换为 void*
float* float_ptr3 = (float *)void_ptr; // void* -> float* 隐式转换无效
float* float_ptr4 = static_cast<float*>(void_ptr); // void* -> float* 使用 static_cast 转换成
功
char* char_ptr3 = (char *)void_ptr; // void* -> char* 隐式转换无效
char* char_ptr4 = static_cast<char*>(void_ptr); // void* -> char* 使用 static_cast 转换成
功
}
3)不能转换掉 expression 的 const 或 volitale 属性
#include <iostream>
int main(int argc, char* argv[])
{
int temp = 10;
const int* a_const_ptr = &temp;
int* b_const_ptr = static_cast<int*>(a_const_ptr); // const int* -> int* 无效
const int a_const_ref = 10;
int& b_const_ref = static_cast<int&>(a_const_ref); // const int& -> int& 无效
volatile int* a_vol_ptr = &temp;
int* b_vol_ptr = static_cast<int*>(a_vol_ptr); // volatile int* -> int* 无效
volatile int a_vol_ref = 10;
int& b_vol_ref = static_cast<int&>(a_vol_ref); // volatile int& -> int& 无效
}
169、C++类型转换-static_cast
C 风格的类型转换很容易理解:
语法:(目标类型)表达式或目标类型(表达式);
C++认为 C 风格的类型转换过于松散,可能会带来隐患,不够安全。
C++推出了新的类型转换来替代 C 风格的类型转换,采用更严格的语法检查,降低使用风险。
C++新增了四个关键字 static_cast、const_cast、reinterpret_cast 和 dynamic_cast,用于支持 C++
风格的类型转换。
C++的类型转换只是语法上的解释,本质上与 C 风格的类型转换没什么不同,C 语言做不到事情的
C++也做不到。
语法:
static_cast<目标类型>(表达式);
const_cast<目标类型>(表达式);
reinterpret_cast<目标类型>(表达式);
dynamic_cast<目标类型>(表达式); 一、static_cast
1)用于内置数据类型之间的转换
除了语法不同,C 和 C++没有区别。
#include <iostream>
using namespace std;
int main(int argc, char* argv[])
{
int ii = 3;
long ll = ii; // 绝对安全,可以隐式转换,不会出现警告。
double dd = 1.23;
long ll1 = dd; // 可以隐式转换,但是,会出现可能丢失数据的警告。
long ll2 = (long)dd; // C 风格:显式转换,不会出现警告。
long ll3 = static_cast<long>(dd); // C++风格:显式转换,不会出现警告。
cout << "ll1=" << ll1 << ",ll2=" << ll2 << ",ll3=" << ll3 << endl;
}
2)用于指针之间的转换
C 风格可以把不同类型的指针进行转换。
C++不可以,需要借助 void *。
#include <iostream>
using namespace std;
void func(void* ptr) { // 其它类型指针 -> void *指针 -> 其它类型指针
double* pp = static_cast<double*>(ptr);
}
int main(int argc, char* argv[])
{
int ii = 10;
//double* pd1 = ⅈ // 错误,不能隐式转换。
double* pd2 = (double*) ⅈ // C 风格,强制转换。
//double* pd3 = static_cast<double*>(&ii); // 错误,static_cast 不支持不同类型指针的
转换。
void* pv = ⅈ // 任何类型的指针都可以隐式转换成 void*。
double* pd4 = static_cast<double*>(pv); // static_cast 可以把 void *转换成其它类型的指
针。
func(&ii);
}
二、const_cast
static_cast 不能丢掉指针(引用)的 const 和 volitale 属性,const_cast 可以。
示例:
#include <iostream>
using namespace std;
void func(int *ii)
{}
int main(int argc, char* argv[])
{
const int *aa=nullptr;
int *bb = (int *)aa; // C 风格,强制转换,丢掉 const 限定符。
int* cc = const_cast<int*>(aa); // C++风格,强制转换,丢掉 const 限定符。
func(const_cast<int *>(aa));
}
三、reinterpret_cast
static_cast 不能用于转换不同类型的指针(引用)(不考虑有继承关系的情况),reinterpret_cast
可以。
reinterpret_cast 的意思是重新解释,能够将一种对象类型转换为另一种,不管它们是否有关系。
语法:reinterpret_cast<目标类型>(表达式);
<目标类型>和(表达式)中必须有一个是指针(引用)类型。
reinterpret_cast 不能丢掉(表达式)的 const 或 volitale 属性。
应用场景:
1)reinterpret_cast 的第一种用途是改变指针(引用)的类型。
2)reinterpret_cast 的第二种用途是将指针(引用)转换成整型变量。整型与指针占用的字节数必
须一致,否则会出现警告,转换可能损失精度。
3)reinterpret_cast 的第三种用途是将一个整型变量转换成指针(引用)。
示例:
#include <iostream>
using namespace std;
void func(void* ptr) {
long long ii = reinterpret_cast<long long>(ptr);
cout << "ii=" << ii << endl;
}
int main(int argc, char* argv[])
{
long long ii = 10;
func(reinterpret_cast<void *>(ii));
}