一、#ifndef/#define/#endif
- 有很多头文件中会出现上面的代码结构,目的是为了防止当前头文件被重复引用。
- “被重复引用”是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。比如:存在a.h文件#include “c.h”,而此时b.cpp文件导入#include "a.h"和#include "c.h"此时就会造成c.h重复引用。
- 头文件被重复引用引起的后果:
- 有些头文件重复引用只是增加了编译工作的工作量,不会引起太大问题,仅仅是编译效率低了一些,但是对于大工程而言编译效率低下是很痛苦的一件事。
- 有些头文件重复引用,会引起错误,比如在头文件中定义了全局变量,此时会引起重复定义。
- 下面给一个#ifndef/#define/#endif的格式:
#ifndef A_H 意思是if not define a.h 如果不存在a.h
#define A_H 就引入a.h
#endif 否则不需要引入
二、命名空间
- 引入命名空间的概念,作为附加信息区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上命名空间就是定义了一个范围。
- 定义命名空间
namespace namespace_name{ // 代码声明 }
- 调用命名空间的函数或变量,需要在前面加上命名空间的名称
name::code // code 可以是变量或函数
- using指令,可以使用using namespace指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称
using namespace namespace_name
- 不连续的命名空间
命名空间可以定义在几个不同的部分中,因此命名空间是由几个单独定义的部分组成。下面的命名空间定义可以使定义一个新的命名空间,也可以是为已有的命名空间增加新的元素:namespace namespace_name{ //代码声明 }
- 嵌套的命名空间
命名空间可以嵌套,可以再一个命名空间中定义另一个命名空间namespace namespace_name{ namespace namespace_name{ //代码声明 } }
三、接口(抽象类)
- 接口描述了类的行为和功能,而不需要完成类的特定实现
- C++接口是使用抽象类(abstract class,通常称为ABC),抽象类和数据抽象互不混淆,数据抽象是一个把实现细节和相关数据分离开的概念
- 数据抽象,是一种依赖于接口和实现分离的编程(设计)技术。
- 在实际实现中,如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类,纯虚函数实在通过申明中使用"=0"来指定的,如下所示:
class Box { public: // 纯虚函数 virtual double getVolume() = 0; private: double length; // 长度 double breadth; // 宽度 double height; // 高度 };
- 设计抽象类的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,只能作为接口使用。
- 如果试图实例化一个抽象类的对象,会导致编译错误。
- 如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,对导致编译错误
四、迭代器
- 迭代器是一个变量相当于容器和操作容器的算法之间的中介,迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素,从这一点看,迭代器和指针类似,可以看做是容器专属指针。
- 迭代器按照定义方式分成以下四种:
- 正向迭代器,定义方法如下:
容器类名::iterator 迭代器名
- 常量正向迭代器
容器类名::const_iterator 迭代器名
- 反向迭代器
容器类名::reverse_iterator 迭代器名
- 常量反向迭代器
容器类名::const_reverse_iterator 迭代器名
- 正向迭代器,定义方法如下:
- 迭代器用法实例
- 通过迭代器可以读取它指向的元素,
*迭代器
就表示迭代器指向的元素,通过非常量迭代器还能修改其指向的元素。 - 迭代器都可以进行++操作。反向迭代器和正向迭代器的区别在于:
- 对正向迭代器进行++操作时,迭代器会指向容器的后一个元素
- 而对反向迭代器进行++操作时,迭代器会指向容器中的前一个元素
- 演示通过迭代器遍历一个vector容器中的所有元素
程序输出结果:#include <iostream> #include <vector> using namespace std; int main() { vector<int> v; //v是存放int类型变量的可变长数组,开始时没有元素 for (int n = 0; n<5; ++n) v.push_back(n); //push_back成员函数在vector容器尾部添加一个元素 vector<int>::iterator i; //定义正向迭代器 //.begin()指向第一元素,.end()指向最后一个元素后面的位置 for (i = v.begin(); i != v.end(); ++i) { //用迭代器遍历容器 cout << *i << " "; //*i 就是迭代器i指向的元素 *i *= 2; //每个元素变为原来的2倍 } cout << endl; //用反向迭代器遍历容器 for (vector<int>::reverse_iterator j = v.rbegin(); j != v.rend(); ++j) cout << *j << " "; return 0; }
0 1 2 3 4 8 6 4 2 0
- 通过迭代器可以读取它指向的元素,
五、容器
1、容器的定义
在数据存储上,有一种对象类型,它可以持有其他对象或指向其他对象的指针,这种对象类型就叫容器。
2、容器的种类
- 顺序容器:是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集。顺序容器包括:vector(向量)、list(列表)、deque(队列)。
- 关联容器:关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系。关联容器包括:map(映射)、set(集合)、mutimap(多重映射)、mutiset(多重集合)。
- 容器适配器: 适配器是容器的接口,它本身不能直接保存元素,它保存元素的机制是调用另一种顺序容器取实现。STL中包含三种适配器:栈stack、队列queue和优先级队列priority_queue
不同容器的具体使用方法可以参考下面的资料:
1. https://blog.csdn.net/u014465639/article/details/70241850 //介绍的比较细。
2. https://blog.csdn.net/yuleidnf/article/details/81541736 //样例代码写的比较清晰
六、指针
1、定义:
指针是一个其值为地址的变量(就是一个存储地址的变量)
2、基本运算符;
& 取地址运算符
* 间接访问运算符/解引用指针
&运算符,后面跟一个变量名的时候,给出该变量名的地址。
-
运算符,后面跟一个指针,得到指针指向的内存中的内容。
#include <iostream> using namespace std; int main() { int a=5; double b=10.4; cout<<"Address of a:"<<&a<<endl; cout<<"Address of b:"<<&b<<endl; cout<<"a:"<<*(&a)<<endl; }
运行输出:
Address of a:0x69fefc
Address of b:0x69fef0
a:5
3、指针声明
typename *name
注意:为了不和解引用的* 号相混淆,也有让* 号紧挨着typename的写法(比如int* p_a,表示声明的是int类型的指针变量)
4、 NULL指针和void*
- NULL指针表示不指向任何东西的指针,是一个定义在标准库中值为零的常量
代码执行结果:#include <iostream> using namespace std; int main () { int *ptr = NULL; cout << "ptr 的值是 " << ptr ; return 0; }
注:C++ 11中新引入一种特殊类型的字面值ptr 的值是 0
nullptr
来初始化指针为空指针 - void* 是一种特殊类型的指针,能够用来存放任何类型对象的地址
#include <iostream> int main() { double x=25.5; double *p=&x; //void* 类型可以接受任意类型对象地址 void *p_v=&x; void *p_v2=p; std::cout<<"p_v:"<<p_v<<std::endl; std::cout<<"p_v2:"<<p_v2<<std::endl; }
p_v:0x69fee8 p_v2:0x69fee8
- 有很多错误都是由于解引用没有初始化的指针引起的,所以尽量在定义了的对象后在定义指向这个对象的指针,对于不清楚指向哪里的指针,一律初始化为nullptr。
5、指向指针的指针
指向指针的指针是一种多级间接寻址的形式,指针的指针就是将指针的地址存在在另一个指针里,如下图
指向指针的指针必须如下声明。即在变量名前放置两个星号。例如声明一个指向int类型指针的指针:
int **val
访问目标值时,也是使用两个星号,直接看代码:
#include <iostream>
int main()
{
int a=10;
int *p_a=&a;
int **pp_a=&p_a;
std::cout<<"p_a:"<<p_a<<std::endl<<"*p_a:"<<*p_a<<std::endl;
std::cout<<std::endl;
std::cout<<"PP_a:"<<pp_a<<std::endl \
<<"*pp_a:"<<*pp_a<<std::endl \
<<"**pp_a:"<<**pp_a<<std::endl;
}
p_a:0x69fee8
*p_a:10
PP_a:0x69fee4
*pp_a:0x69fee8
**pp_a:10
6、指针和数组、指针数组、指针和字符串
指针和数组:
- 一看到数组,就要知道数组名可以当作这个数组的第一个地址(a = &a[0]),但数组名是一个指向数组开头的常量不可更改。
- 对于指向数组的指针,指针+n表示往后移动n个位置
- 指针也可以像数组那样用p_a[n]这种形式直接取元素,本质是*(p_a + n)
指针数组:
指针数组可以简单的理解成,一个数组里面存放的是都是指针。
声明方式如下:
int *ptr[3] //声明一个指针数组,有3个整数指针组成
ptr中的每个元素都是一个指向int类型的值的指针。代码实例如下:
#include <iostream>
using namespace std;
const int MAX = 3;
int main ()
{
int var[MAX] = {10, 100, 200};
int *ptr[MAX];
for (int i = 0; i < MAX; i++)
{
ptr[i] = &var[i]; // 赋值为整数的地址
}
for (int i = 0; i < MAX; i++)
{
cout << "Value of var[" << i << "] = ";
cout << *ptr[i] << endl;
}
return 0;
}
代码编译执行结果:
Value of var[0] = 10
Value of var[1] = 100
Value of var[2] = 200
指针和字符串
指针和字符串有几个比较重要的规则:
- C风格的字符串本质就是一个字符数组,所以数组名就是第一个元素的地址。
- C++中对于引号引起来的字符串,也代表第一个元素的地址。所以对于字符指针可以直接用字符串赋值
cout
对象认为,char
的地址是字符串的地址,因此它打印该地址处的字符,并继续打印后面的字符,直到遇到空字符\0
之后才停止。
举例详细说明:
#include <iostream>
#include <cstring>
int main()
{
char animal[20]="bear";
const char* bird="wren";
char* p_s;
//数组名也是指针
std::cout<<animal<<" and "<<bird<<std::endl;
//直接输出指针
p_s=animal;
std::cout<<"p_s:"<<p_s<<std::endl;
//想要输出真的地址怎么办?
std::cout<<"address of animal:"
<<(int*)animal<<std::endl;
return 0;
}
代码编译运行结果:
bear and wren
p_s:bear
address of animal:1918985570
7、指针类型
提个小问题:
既然指针的本质都是变量的内存首地址,即一个int类型的整数。
- 那为什么还要有各种类型呢?
- 比如int指针,float指针,这个类型影响了指针本身存储的信息吗?
- 这个类型会在什么时候发挥作用
指针存储的是变量内存的首地址,那编译器怎么知道该从首地址开始取多少个字节呢?
这就是指针类型发挥作用的时候,编译器会根据指针的所指元素的类型取判断应该取多少个字节。
例如,如果是int型的指针,编译器会产生提取四个字节的指令,char则只提取一个字节,以此类推。
指针类型强制转换
举个例子:
float f = 1.0;
short c = *(short*)&f;
对于f变量,在内存层面发生了什么变化?
或者c的值是多少?1?
实际上,从内存层面来说,f什么都没变
如图:
*(short*)&f
的详细过程如下:
&f
取得f
的首地址(short*)&f
,类型强制转换,实际上就是表明f
这个地址放的是一个short类型的变量*(short*)&f
,解引用时,编译器会取出前面两个字节,并按照short的编码方式去解释,并将解释出的值赋给c变量
当然最后的值肯定不是1,至于是什么可以去真正算一下
反过来的话:
short c = 1;
float f = *(float*)&c;
具体过程和上面是一样的,但是这里会存在风险。
(float*)&c
表示从c
的首地址开始取四个字节,然后按照float的编码方式去解释。
但是c
是short类型只占两个字节,那肯定会访问到相邻后面两个字节,这时就会发生内存访问越界。
如果只是读,大概率是没问题的,最多可能出现读取的值不大对。
但是当向这个区域写入新的值,比如:
*(float*)&c = 1.0;
这就可能会发生coredump,也就是访存失败。即使不会发生coredump,也会破坏这块内存原有的值,会导致隐藏的BUG。