C++ 学习总结

一、#ifndef/#define/#endif

  1. 有很多头文件中会出现上面的代码结构,目的是为了防止当前头文件被重复引用。
  2. “被重复引用”是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。比如:存在a.h文件#include “c.h”,而此时b.cpp文件导入#include "a.h"和#include "c.h"此时就会造成c.h重复引用。
  3. 头文件被重复引用引起的后果:
    1. 有些头文件重复引用只是增加了编译工作的工作量,不会引起太大问题,仅仅是编译效率低了一些,但是对于大工程而言编译效率低下是很痛苦的一件事。
    2. 有些头文件重复引用,会引起错误,比如在头文件中定义了全局变量,此时会引起重复定义。
  4. 下面给一个#ifndef/#define/#endif的格式:
    #ifndef A_H 意思是if not define a.h 如果不存在a.h
    #define A_H 就引入a.h
    #endif 否则不需要引入

二、命名空间

  1. 引入命名空间的概念,作为附加信息区分不同库中相同名称的函数、类、变量等。使用了命名空间即定义了上下文。本质上命名空间就是定义了一个范围。
  2. 定义命名空间
    namespace namespace_name{
    // 代码声明
    }
    
  3. 调用命名空间的函数或变量,需要在前面加上命名空间的名称
    name::code // code 可以是变量或函数
  4. using指令,可以使用using namespace指令,这样在使用命名空间时就可以不用在前面加上命名空间的名称
    using namespace namespace_name
  5. 不连续的命名空间
    命名空间可以定义在几个不同的部分中,因此命名空间是由几个单独定义的部分组成。下面的命名空间定义可以使定义一个新的命名空间,也可以是为已有的命名空间增加新的元素:
    namespace namespace_name{
       //代码声明
    }
    
  6. 嵌套的命名空间
    命名空间可以嵌套,可以再一个命名空间中定义另一个命名空间
    namespace namespace_name{
    	namespace namespace_name{
    	  //代码声明
    	}
    }
    

三、接口(抽象类)

  1. 接口描述了类的行为和功能,而不需要完成类的特定实现
  2. C++接口是使用抽象类(abstract class,通常称为ABC),抽象类和数据抽象互不混淆,数据抽象是一个把实现细节和相关数据分离开的概念
    • 数据抽象,是一种依赖于接口和实现分离的编程(设计)技术。
  3. 在实际实现中,如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类,纯虚函数实在通过申明中使用"=0"来指定的,如下所示:
    class Box
    {
       public:
          // 纯虚函数
          virtual double getVolume() = 0;
       private:
          double length;      // 长度
          double breadth;     // 宽度
          double height;      // 高度
    };
    
  4. 设计抽象类的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,只能作为接口使用。
    • 如果试图实例化一个抽象类的对象,会导致编译错误。
    • 如果没有在派生类中重写纯虚函数,就尝试实例化该类的对象,对导致编译错误

四、迭代器

  1. 迭代器是一个变量相当于容器和操作容器的算法之间的中介,迭代器可以指向容器中的某个元素,通过迭代器就可以读写它指向的元素,从这一点看,迭代器和指针类似,可以看做是容器专属指针。
  2. 迭代器按照定义方式分成以下四种:
    1. 正向迭代器,定义方法如下:
      容器类名::iterator 迭代器名
    2. 常量正向迭代器
      容器类名::const_iterator 迭代器名
    3. 反向迭代器
      容器类名::reverse_iterator 迭代器名
    4. 常量反向迭代器
      容器类名::const_reverse_iterator 迭代器名
  3. 迭代器用法实例
    • 通过迭代器可以读取它指向的元素,*迭代器就表示迭代器指向的元素,通过非常量迭代器还能修改其指向的元素。
    • 迭代器都可以进行++操作。反向迭代器和正向迭代器的区别在于:
      • 对正向迭代器进行++操作时,迭代器会指向容器的后一个元素
      • 而对反向迭代器进行++操作时,迭代器会指向容器中的前一个元素
    • 演示通过迭代器遍历一个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、容器的种类

  1. 顺序容器:是一种各元素之间有顺序关系的线性表,是一种线性结构的可序群集。顺序容器包括:vector(向量)、list(列表)、deque(队列)。
  2. 关联容器:关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系。关联容器包括:map(映射)、set(集合)、mutimap(多重映射)、mutiset(多重集合)。
  3. 容器适配器: 适配器是容器的接口,它本身不能直接保存元素,它保存元素的机制是调用另一种顺序容器取实现。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*

  1. NULL指针表示不指向任何东西的指针,是一个定义在标准库中值为零的常量
    #include <iostream>
    using namespace std;
    
    int main ()
    {
       int  *ptr = NULL;
       cout << "ptr 的值是 " << ptr ;
       return 0;
    }
    
    代码执行结果:
    ptr 的值是 0
    
    注:C++ 11中新引入一种特殊类型的字面值nullptr来初始化指针为空指针
  2. 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
    
  3. 有很多错误都是由于解引用没有初始化的指针引起的,所以尽量在定义了的对象后在定义指向这个对象的指针,对于不清楚指向哪里的指针,一律初始化为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、指针和数组、指针数组、指针和字符串
指针和数组

  1. 一看到数组,就要知道数组名可以当作这个数组的第一个地址(a = &a[0]),但数组名是一个指向数组开头的常量不可更改。
  2. 对于指向数组的指针,指针+n表示往后移动n个位置
  3. 指针也可以像数组那样用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

指针和字符串
指针和字符串有几个比较重要的规则:

  1. C风格的字符串本质就是一个字符数组,所以数组名就是第一个元素的地址。
  2. C++中对于引号引起来的字符串,也代表第一个元素的地址。所以对于字符指针可以直接用字符串赋值
  3. 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类型的整数。

  1. 那为什么还要有各种类型呢?
  2. 比如int指针,float指针,这个类型影响了指针本身存储的信息吗?
  3. 这个类型会在什么时候发挥作用

指针存储的是变量内存的首地址,那编译器怎么知道该从首地址开始取多少个字节呢?
这就是指针类型发挥作用的时候,编译器会根据指针的所指元素的类型取判断应该取多少个字节。
例如,如果是int型的指针,编译器会产生提取四个字节的指令,char则只提取一个字节,以此类推。
指针类型强制转换
举个例子:

float f = 1.0;
short c = *(short*)&f;

对于f变量,在内存层面发生了什么变化?
或者c的值是多少?1?
实际上,从内存层面来说,f什么都没变
如图:
在这里插入图片描述

*(short*)&f的详细过程如下:

  1. &f取得f的首地址
  2. (short*)&f,类型强制转换,实际上就是表明f这个地址放的是一个short类型的变量
  3. *(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。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值