一词三问(C++)

一词三问

linux进程地址空间布局

“分段”:把程序中的各种数据,分不同的区域(“段”)来存储。

linux对进程的数据进行分段管理,不同属性的数据,存储在不同的“内存段”
不同的“内存段(内存区域)”的属性及管理方法不一样。

  • .text 段

主要存放 代码(指令)
只读并且共享,这段内存在程序运行期间(进程存活时间),不会释放。
"代码段"的生存期: 随程序持续性(随进程持续性)

  • .data 段

数据段。
主要存放程序中的 已经初始化的全局变量 和 已经初始化的static 变量。
可读可写,这段内存在进程运行期间,一直存在。
.data 段的生存期: 随进程持续性。

  • .bss段

数据段。
主要存放程序中的 未初始化的全局变量 和 未初始化的static变量。
.bss段在进行运行前,系统会初始化 0。
可读可写,这段内存在进程运行期间,一直存在。
.bss 段的生存期: 随进程持续性。

int a = 5;
int c; 
void func(void)
{
    static int b = 6;

}


int main()
{
    static int d;
}

上面这个例子, 全局变量 a 和 static变量b 保存在 .data段中
全局变量 c 和 static变量d 保存在 .bss段中。

  • .rodata 段

read only data 只读数据段
主要是存放程序中的只读数据(常量),如: 字符串常量 “abcde”
只读。这段内存在进程运行期间,一直存在。
.rodata的生存期:随进程持续性

  • 栈 空间(stack)

主要存放 局部变量(非static的局部变量)
可读可写。 栈空间,会自动释放(代码块执行完了,代码块中局部 变量的空间变会自动释放)
栈空间的变量的生存期: 随代码块持续性。

// int m = 250;
// int sb[m];

void func()
{
    int a = 5;

    a++;

    printf("a = %d\n", a);
}

int*  get_addr()
{
    int x = 100;
    //....

    return &x;
}
//NOTE:
// "返回一个局部变量的地址  是有问题"

int main()
{
    int n;
    scanf("%d");

    int sb[n]; //可以编译过去,但是:不建议这样搞,为什么?
                // 优秀的程序员,写不出这么垃圾的代码.

 
    func();
    func();
}

  • 堆空间 (heap)

动态内存空间。主要是由 malloc/realloc/calloc动态分配的空间。
可读可写。这段内存 在运行期间,一旦分配,就会一直存在,直到你手动free或进程结束。
这段空间,我们称之为 堆空间heap
堆空间只能通过 地址去访问它。

防止"内存泄漏/垃圾内存"

int main()
{
    int *p =  malloc(1024);
    
    //....

    p = &a;


}


对象

  • 在面向对象程序设计中,所有东西都可以是对象。每个对象都有自己的属性和行为。

  • 类是同类型对象的抽象,包含该类型所有对象都具有的属性和行为。

  • 在设计和编码阶段,先定义好类型,然后在类型的基础上创建对象

构造函数

  • 一个类的特殊的成员函数,在任何时候创建一个对象时被自动地调用

  • 初始化对象

  • 构造函数的函数名必须和类名一样

    构造函数没有返回类型

    构造函数在创建一个类的对象时,系统自动调用它

    构造函数可以带参数,也可以不带参数

    一个类可以有多个构造函数,只要它们的参数互不相同

    一个类必须要有构造函数

    执行构造函数的第一条语句前,先执行“初始化列表”

析构函数

  • 一个类的特殊成员函数,会在对象销毁时,自动调用它

  • 为了在销毁对象时,自动回收一个对象的资源

    在对象销毁(对象的生命周期结束)时,系统自动调用这个析构函数

  • 析构函数的函数名为:~类名()

  • 析构函数不带参数,也没有返回值(void也不需要)

  • 一个类只能有一个析构函数

new/delete和malloc/free的区别

  • new和malloc都是在堆中分配空间
  • new是堆中创建一个对象,并返回其指针
  • malloc仅在堆中分配一块空间,并返回其地址
  • new的时候,会调用对象的构造函数;malloc你不会
  • new分配的对象,销毁时必须要用delete
  • malloc分配的空间,释放时必须要用free
  • new/delete是运算符,malloc/free是一个库函数

封装Encapsulation

  • 类把对象共有的属性和行为组织在一个类里面,并且规定不同的属性和行为的访问权限(private/protected/public),这就是封装
  • 把操作方法(成员函数)和数据(属性)封装在一个对象(类)里面

类的静态成员

  • 一个类的所有对象之间共享数据,而不是所有对象共享数据(其他类的对象不能共享);保证数据的安全。
  • 为了实现类的对象之间数据共享,同时又能保证数据的安全,提出了这个概念
  • 用static修饰的类的成员,称为类的静态成员
  • 静态成员不属于某个对象,而是属于这个类的所有对象,“属于类”
  • 静态数据成员的初始化,必须放在类外:类型 类名::静态数据成员名 = 初始值;

静态成员函数

  • 加了static修饰的成员函数,称之为类的静态成员函数

  • static和非static成员函数的区别

    含义上的区别:

    • 加了static表示这个成员函数属于类
    • 不加static表示这个成员函数属于对象

    static成员函数没有this指针

    • static成员函数中,不能访问非static成员,非static成员属于对象

    访问方式的区别:

    • 类的非static成员函数,只能通过对象名或对象指针去访问
    • 类的static成员函数,可以用对象名或对象指针去访问,还可以用“类名::函数名”访问

    类的非static成员函数,第一个参数其实是隐含的“this指针”

    类的非static成员函数,后面可以用const修饰,类的static函数不能用const修饰(const其实修饰的是this)

静态成员的访问方法

  • 通过静态成员函数去访问静态成员变量
  • 可以通过非static成员函数去访问静态成员
  • 不能通过static成员函数去访问非static成员变量

引用

  • 引用即别名,是一个已经存在的数据对象的另外一个名字
  • 语法:类型 &别名 = 已经存在的数据对象(变量/对象);
  • “类型”:引用对象的类型,已经存在的数据对象的类型
  • 引用在声明时,必须同时初始化
  • 引用一旦声明,就不能更改
  • 引用不对应“独立的内存空间”,是一个已经命名的空间的另外一个名字而已
  • 引用的好处:
    • 更安全
    • 更容易
    • 可以避免“内存拷贝”
  • 主要用于:
    • 函数形参
    • 函数返回值

引用和指针

  • 引用是别名,指针保存的是指向对象的地址
  • 引用本身是没有地址的,指针变量本身是有地址的
  • 指针是可以载赋值的,引用则不行
  • 指针可以被赋值为nullptr(NULL),但是引用不可以被赋值为空
  • 指针可以有多级指针,引用则只有一层引用

拷贝构造函数

  • 也是类的构造函数,作用是:初始化对象

  • 拷贝构造函数初始化对象的方式,是通过“一个已经存在的对象去初始化新对象”

  • 函数原型:T(const T& a){},参数为带一个本类对象的常引用

  • 调用拷贝构造函数的情况

    • 声明一个新对象时,()/{}内是另外一个已经有的对象

      A t2(t1);
      
    • 声明一个新对象时,用一个已经存在的对象来赋值

      A t2 = t1;
      

深拷贝(Deep Copy)和浅拷贝(Shallow Copy)

  • 深拷贝不仅拷贝当下的对象内存,对象中指向的空间或资源(以及指向的空间的n次方)都会重新分配并拷贝
  • 浅拷贝之拷贝当前的对象内存,对象中指向的空间没有重新分配及拷贝

右值引用

  • 左值引用:引用到一个具体的“可寻址的内存”上的引用,左值引用
  • 右值引用
    • 引用到一个右值(“只能读”)的引用,称为右值引用
    • 右值引用,就是必须绑定右值的引用,引用一个右值
    • 右值要么是字面常量,要么是在表达式求值过程中创建的临时对象
    • 右值引用也是某个对象的另外一个名字
    • 右值引用会为字面常量值,创建一个临时空间
    • 右值引用把你的临时对象的作用域或生存期扩大或延长
    • 右值引用主要是用来绑定一个将要销毁的对象上,使用右值引用的代码可以自由接管所引用的对象的资源
  • 语法:(引用对象的)类型 && 引用名 = 所引用的字面常量或临时对象;
  • const引用
    • 常引用:引用到一个常量值,表示我只是“读你的值”
    • 把一个左值引用 引用到 常引用 是可以的
    • 把一个右值引用 引用到 常引用 也是可以的

移动构造函数

  • 移动语义:当用一个“临时对象(做完马上被销毁)“去构造一个新对象时,当新对象构造完成时,”临时对象“马上被销毁。
  • 移动:把临时对象的资源直接移交给新对象
  • 如果编译器检测到构造新对象时,传进来的那个对象是一个“马上要被销毁的对象”->“临时对象“,构造函数需要接收一个”临时对象的引用“->右值引用
  • 原型:T(T &&) noexcept{}
  • 调用时机:当用一个“即将被销毁的对象”,去构造一个新对象时,编译器自动选择调用你的移动构造函数
  • 实现要求:让新对象去接管临时对象的资源

友元

  • friend”朋友“,友元机制是对类的封装机制的一种补充。
  • 友元机制使得另一个类或函数,可以访问一个其他类对象的private/protected成员
  • 友元函数/友元类

友元函数

  • 如果一个函数func是另外一个类A的友元(朋友)
  • 那么这个函数func就可以访问类A对象的private/protected成员
  • 此时,把函数func称为类A的友元函数
  • 声明:friend 函数声明;

友元类

  • 如果类A是类B的朋友,那么类A的所有成员函数(“类A的作用域中”),都可以访问类B的对象的private/protected成员
  • 声明:friend class 类名;
  • 把上述声明放在指定的类B中,那么这个类就是类B的友元

友元传递

  • 友元关系的单向的

    类A是类B的友元,无法退出:类B是类A的友元

  • 友元关系是不可传递的

    类A是类B的友元,类B是类C的友元,不能推出:类A是类C的友元

命名空间

  • 命名空间的本质是一块内存(”作用域“),用来存放一些常量、变量、函数、类型等
  • 目的:避免“名字冲突”
  • 语法:namespaece 命名空间的名称{/*常量、变量、函数等定义或声明*/};
  • 如果定义的namespace需要被多个文件包含,则需要把声明和定义分开。如果不分开,则会报“xxx被多次定义 multiple definition”。声明放在头文件中,定义或实现放在相应的cpp中,以防止重复定义

命名空间的使用方法

  • 使用“作用域限定符::”来访问命名空间中的成员

  • using声明

  • 无名字的命名空间

    定义一个命名空间时,不指定名字

    特点:在“无名命名空间”中定义的常量、变量、函数等对象被设置为全局变量,违背了命名空间的设定原则,所以一般不使用无名命名空间。程序中定义的全局变量,会自动加入到无名命名空间中去,要访问“无名命名空间中的成员”,要用::成员名

继承Inheritance

  • 一个类自动得到其父类(基类,另外一个类)的所有属性和行为
  • 目的:代码复用,实现快速开发
  • 语法:class 派生类名 : 继承方式 基类名 {, 继承方式 基类名2, ...}{};
  • 继承方式:决定派生类对基类继承过来的成员的访问权限,基类成员的可见性

三种继承方式的区别

Base Class成员public继承private继承protected继承
private members不可见不可见不可见
protected membersprotectedprivateprotected
public memberspublicprivateprotected
  • 派生类虽然继承了基类所有属性和行为,但不一定能访问它们,基类的private,派生类就不能访问

派生类不能访问的

  • 基类的私有成员,派生类不能访问
  • 基类的友元,派生类不能继承
  • 基类的赋值运算符,派生类不能继承
  • 基类的构造函数和析构函数,派生类不能继承

派生类访问基类的构造函数和析构函数

  • 系统会自动调用合适的构造函数和析构函数
  • 派生类的构造函数自动调用基类的默认构造函数(无参构造或等同于无参构造),编译器不知道如何调用基类的构造函数,因为要传递参数
  • 显示调用基类的非默认构造函数:派生类构造函数的初始化列表中,可以指定调用基类的任意构造函数:derived_constructor(xxx): base_constructor(yyy){}

多继承

  • 一个派生类可以同时继承多个基类的属性和行为
  • 语法:class 派生类名: 继承方式 基类A, 继承方式 基类B, ...{};

派生类、基类、组合对象构造函数调用顺序

  • 一个对象的内存布局(数据成员,non-static数据成员)
  • 继承是is a的关系,组合对象是has a的关系
  • 先布局基类的数据成员(按继承的先后顺序,依次分配空间),然后才是派生类自己的数据成员(按成员的声明顺序,依次分配空间)
  • 调用顺序:按继承先后调用基类的构造函数→按成员的声明顺序,依次调用各数据成员的构造函数
  • 按内存布局的先后,依次调用相应的构造函数
  • 调用析构函数的顺序则刚好相反

基类与派生类赋值兼容的问题

  • 派生类对象就是一个基类对象
  • 程序中所有需要基类对象的地方,可以用派生类替换
  1. 可以用派生类对象去初始化基类对象

    因为派生类对象中,包含基类的数据成员

  2. 可以把派生类对象赋值给基类对象

    等于号右边的对象类型一定要兼容左边的那个对象

  3. 可以把派生类对象初始化为基类的引用

  4. 基类指针可以指向派生类对象

多态Polymorphism

  • 不同的对象,在收到相同的信息时,产生不同的行为;指向类型不同的对象,在收到相同的消息,实现不同的算法功能。

  • 具体表现:

    编译时刻呈现的多态

    1. 函数重载
    2. 运算符重载

    运行时刻的多态

    1. 虚函数

重载Overloading

  • 在C++中,如果我们创建两个或以上的成员,它们有相同的名字,但是参数个数或类型不一样,这就是“重载”,“名字相同,参数不同”

函数重载

  • 在同一个作用域上,多个函数同名,但参数个数或类型不一样

    C++中,const int 和 int 是不同的类型

  • 目的:提高代码的可读性和可维护性

    通过函数重载,程序员可以使用相同的名称来表示不同的操作,而不必记住多个不同的名称

  • 为了支持函数重载(多个同名函数,但参数不同),采用了一种叫“换名机制”的东西。重载函数在经过编译器编译后,编译器会结合参数的个数及类型,形成一个新的函数名,这种机制称为C++编译器的“换名机制”

  • 引起函数重载歧义的原因

    1. 类型转换引起的歧义
    2. 带默认参数的函数重载,引起的歧义
    3. 带“引用参数”的函数,引起的歧义

extern “C”

  • 混合编程时,C++采用了换名机制,C语言没有换名机制
  • 目的:为了支持C++与C混合编程
  • 加上extern "C"后,会指示编译器这部分的代码按C语言,而不是C++的方式进行编译

运算符重载 Operator Overloading

  • 赋予运算符接不同类型操作数的功能

  • 目的:方便程序员使用自定义类型,合适的操作符重载可以使自定义类型的操作像内置类型一样自然

  • 所有的运算符都是通过函数来实现的,运算符重载其实就是函数重载

    1. 运算符函数的函数名为:operator运算符

    2. 运算符函数的参数为:运算符的操作数

    3. 运算符函数的返回值:根据运算符表达式的值的类型来定

    4. 分为两类:

      • 全局函数

        return_type operator运算符(arguments){
            
        }
        
      • 类的成员函数

        class T{
            return_type operator运算符(arguments){
                
            }
        };
        
  • 只能实现为类的成员函数的:

    1. 赋值运算符 =
    2. 下标运算符 []
    3. 函数调用运算符 ()
    4. 指针运算符 ->
  • 不能重载的运算符:

    1. 条件运算符 ?:
    2. 成员访问运算符 .
    3. 域运算符 ::
    4. 长度运算符 sizeof
    5. 成员指针访问运算符 -> 和 .
  • 特殊的运算符重载

    1. 自增/自减运算符重载

      class T{
          //前置++
          T& operator++(){
              //先完成“自增”
              return *this;
          }
          //后置++
          T operator++(int){
              T tmp(*this);
              //完成“自增”
              return tmp;
          }
          //前置--
          T& operator--(){
              //先完成“自减”
              return *this;
          }
          //后置--
          T operator--(int){
              T tmp(*this);
              //完成“自减”
              return tmp;
          }
      };
      
    2. 输入输出运算符重载

      输入输出运算符只能重载为 全局函数

      输出运算符的第一操作数是类 ostream 对象

      输入运算符的第一操作数是类 istream 对象

      class Complex{
          friend ostream& operator<<(ostream&, const Complex&);
          friend istream& operator>>(istream&, Complex&);
      };
      
    3. 赋值运算符(=)重载

      赋值运算符函数只能重载为 类的成员函数

      • 防止“自赋值”,如:c1 = c1
      • 当时一个Deep copy时,重新分配资源,实现“Deep Copy”
      • 返回自身对象的应用。return *this
      class T{
          T& operator=(const T& other){
              //1.防止“自赋值”
              if(this == &other){
                  return *this;;
              }
              //2.根据具体的情况,实现“内存拷贝”
              //3.返回自身对象的引用
              return *this;
          }
      };
      
    4. 函数调用运算符()重载

      把一个对象当做是一个“函数调用的对象”

      函数调用运算符只能重载为 类的成员函数

      class T{
          return_type operator()(形参列表){
              
          }
          bool operator()(const double& x1, const double& y1) const{
              return x1 * k + b == y1;
          }
      };
      
      • 函数对象Funcator “仿函数”
      • 在C++中,所有实现了“函数调用运算符()”的类的对象,我们称之为“函数对象”。这个对象,可以当函数使用
      • 函数对象的优势在于:
      • 函数对象本身可以保持一些状态或属性信息
      • 普通函数或函数指针,把操作数据的指针和数据是分开的,函数对象用法比较方便

函数隐藏Function Hiding

  • 函数隐藏发生在派生类和基类当中,假如派生类中定义了一个函数与基类的函数同名(参考可以不同),基类相应的函数就会被隐藏

    通过派生类的实例,就不能调用到基类的相应函数

  • 目的:派生类刻意为之。派生类可能对同一功能,有更好的实现或优化,希望派生类对象,不要再去调用基类的实现

  • 被隐藏后,调用基类被隐藏的函数的方法

    1. 派生类对象.基类名::基类的函数名
    2. 通过 using 声明

函数重写Function Overriding

  • 派生类中定义了与基类一样的函数(函数名相同、参数也相同)
  • 尽管重写了父类的函数,但是派生类中仍然可以通过“父类名::函数名”的方式,去调用父类被重写的函数
  • 通过虚函数表VMT(Virtual Method Table)实现
  • 含有虚函数的类的对象内存的最前面,自动添加一个指针vptr,指向这个虚函数表。
    • VMT 虚函数指针的数组
    • 函数指针的数组 有多少个虚函数,这个数组里面就要多少项
    • 每一项保存的是虚函数的实际(重写)地址

虚函数Virtual Function

  • 在基类中用关键字“virtual”修饰的成员函数,在派生类中可以重写的成员函数
  • 目的:告诉编译器,对这个函数进行一个“动态绑定/延迟绑定”

虚函数的规则

  • 虚函数必须是一个类的 非静态成员函数

  • 虚函数的访问必须通过对象的指针

  • 虚函数必须在基类,即使它在基类中不使用

  • 派生类和基类对于虚函数的原型(函数名、参数)必须完全一致,否则就不是函数重写

  • 构造函数不能声明为虚函数,但是析构函数可以声明为虚函数

    构造函数和析构函数 所有的动态绑定都失效

    构造函数和析构函数 只能进行静态绑定

纯虚函数Pure Virtual Function

  • 只有声明没有实现的虚函数

  • 语法

    class T{
        virtual 返回类型 函数名(参数列表) = 0;
    };
    
  • 包含纯虚函数的类,称为抽象类

  • 抽象类不能实例化对象

    因为抽象类含有纯虚函数,没有实现,没有对象

  • 抽象类的作用

    通过公共的特征和接口,供派生类去继承并实现

模板Template

Template是C++提供的强大的功能特性

允许在设计时,不指定具体的数据类型

“通用类”、“通用函数”、“通用算法”、…、“泛型编程”

Template为“泛型编程”提供支持

  • 泛型编程是一种把类型当参数,目的在于让你的算法支持不同类型的数据结构

  • Template的两种形式

    • Function Template 函数模板
    • Class Template 类模板
  • 语法:

    • 只带一个类型参数

      template <typename T>
      template <class T>
      return_type func_name(parameter_list){
          
      }
      
    • 带多个类型参数的模板

      template <class T1, class T2, ...>
      return_type func_name(parameter_list){
          
      }
      

类模板Class Template

  • 设计类的时候,带“泛型参数”

  • 模板:

    • 一个类型参数的类:模板类名<具体的类型>

      template <class T>
      class class_name{
          T data;
      };
      
    • 带多个类型参数的类模板

      template <typename T1, typename T2, ...>
      class class_name{
          
      };
      //实例化一个对象,或指定类名时
      class_name<T1, T2, ...> object_name;
      
      
  • 模板的声明和实现必须在同一个文件中

迭代器iterator

  • 一个任意的对象,指向一个范围(数组,链表)内的元素
  • 迭代器有遍历这个范围内每个元素的能力,因为迭代器对象实现了一些必要操作(如:++,*)

迭代器模式

  • 提供一种方法顺序访问一个聚合对象中的各个元素,而又不暴露该对象内部结构的表示
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值