C++基础知识

本文详细介绍了C++中的extern、static、volatile和const四个关键字的作用,以及它们在不同场景下的应用。同时,深入探讨了虚函数表的概念,展示了如何通过虚表实现动态绑定和多态。此外,还讨论了浅拷贝和深拷贝的区别,以及抽象类(接口)和多态在C++中的实现。最后,提到了数组指针和指针数组的差异,并强调了在使用new运算符动态分配数组时,正确使用delete和delete[]的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C++基础

1.extern的作用

extern关键字可以用来声明变量和函数作为外部变量或者函数供其它文件使用。

extern “C”
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

因为在C++出现以前,很多代码都是C语言写的,而且很底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能的支持C,而extern "C"就是其中的一个策略。

2.static关键字作用

  1. static修饰局部变量

    (1)静态局部变量使用static修饰符定义,即使在声明时未赋初值,编译器也会把它初始化为0。且静态局部变量存储于进程的全局数据区,即使函数返回,它的值也会保持不变。

  2. static修饰全局变量

    (1)全局变量定义在函数体外部,在全局数据区分配存储空间,且编译器会自动对其初始化。
    (2)普通全局变量对整个工程可见,其他文件可以使用extern外部声明后直接使用。也就是说其他文件不能再定义一个与其相同名字的变量了(否则编译器会认为它们是同一个变量)。
    (3)静态全局变量仅对当前文件可见,其他文件不可访问,其他文件可以定义与其同名的变量,两者互不影响。

  3. static修饰普通函数

    函数的使用方式与全局变量类似,在函数的返回类型前加上static,就是静态函数。其特性如下:
    (1)静态函数只能在声明它的文件中可见,其他文件不能引用该函数
    (2)不同的文件可以使用相同名字的静态函数,互不影响

  4. static修饰成员变量

    在类内数据成员的声明前加上static关键字,该数据成员就是类内的静态数据成员。其特点如下:

    (1)静态数据成员存储在全局数据区静态数据成员在定义时分配存储空间,所以不能在类声明中定义
    (2)静态数据成员是类的成员,无论定义了多少个类的对象,静态数据成员的拷贝只有一个,且对该类的所有对象可见。也就是说任一对象都可以对静态数据成员进行操作。而对于非静态数据成员,每个对象都有自己的一份拷贝。
    (3)由于上面的原因,静态数据成员不属于任何对象,在没有类的实例时其作用域就可见,在没有任何对象时,就可以进行操作
    和普通数据成员一样,静态数据成员也遵从public, protected, private访问规则
    (4)静态数据成员的初始化格式:<数据类型><类名>::<静态数据成员名>=<值>
    (5)类的静态数据成员有两种访问方式:<类对象名>.<静态数据成员名> 或 <类类型名>::<静态数据成员名>
    同全局变量相比,使用静态数据成员有两个优势:

    (1)静态数据成员没有进入程序的全局名字空间,因此不存在与程序中其它全局名字冲突的可能性
    (2)可以实现信息隐藏。静态数据成员可以是private成员,而全局变量不能

  5. static修饰成员函数

    (1)与静态数据成员类似,静态成员函数属于整个类,而不是某一个对象,其特性如下:

    (2)静态成员函数没有this指针,它无法访问属于类对象的非静态数据成员,也无法访问非静态成员函数,它只能调用其余的静态成员函数
    出现在类体外的函数定义不能指定关键字static
    非静态成员函数可以任意地访问静态成员函数和静态数据成员

3.volatile作用?

volatile int i=10;
int a = i;
...
// 其他代码,并未明确告诉编译器,对 i 进行过操作
int b = i;

volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。这样以来,如果 i是一个寄存器变量或者表示一个端口数据就容易出错,所以说 volatile 可以保证对特殊地址的稳定访问。

(1)访问寄存器要比访问内存要快,因此CPU会优先访问该数据在寄存器中的存储器结果,但是内存中的数据可能已经发生改变了,而寄存器中还保留着原来的结果。为了避免这种情况的发生将该变量声明为volatile ,告诉CPU每次都去内存去读取数据。

(2)一个参数既可以是const又可以是volatile 么?–可以,比如只读状态寄存器,

4.const的作用

1)欲阻止一个变量被改变,可使用const,在定义该const变量时,需先初始化,以后就没有机会改变他了;

2)对指针而言,可以指定指针本身为const,也可以指定指针所指的数据为const,或二者同时指定为const;

3)在一个函数声明中,const可以修饰形参表明他是一个输入参数,在函数内部不可以改变其值;

4)对于类的成员函数,有时候必须指定其为const类型,表明其是一个常函数,不能修改类的成员变量;

5)对于类的成员函数,有时候必须指定其返回值为const类型,以使得其返回值不为“左值”。

const修饰变量

​ 变量的值不能改变

const修饰指针

​ 如果const位于的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量
​ 如果const位于
的右侧,const就是修饰指针本身,即指针本身是常量
指针常量:不能通过指针来修改变量的值。
常量指针:一直指向该变量,不能给该指针赋予其他地址

函数中使用const

const修饰函数参数,表示参数不可变,若参数为引用,可以增加效率
const引用传递和函数按值传递的效果是一样的,但按值传递会先建立一个类对象的副本, 然后传递过去,而它直接传递地址,所以这种传递比按值传递更有效
const按值传递时只是外部对象的拷贝,值的改变不会对外部有什么影响,那么是不是没有什么意义:错,重要的目的就是告诉说这个变量不允许被修改,传引用效率会高一点而已。

const 修饰函数返回值

含义和const修饰变量和指针的含义相同???

类中使用const
(1)const修饰成员变量

表示成员变量不能被修改,同时只能在初始化列表中赋值

(2)const修饰成员函数

该函数不能改变对象的成员变量
不能调用非const成员函数,因为任何非const成员函数会有修改成员变量的企图
const的成员函数才能被一个const类对象调用。即const类对象只能调用const成员函数
const关键字不能与static关键字同时使用,因为static关键字修饰静态成员函数,静态成员函数不含有this指针,即不能实例化,const成员函数必须具体到某一实例。

(3)const修饰类对象

对象的任何成员都不能被修改
只能调用const成员函数
问:类中的所有函数都可以声明为const函数吗。哪些函数不能?

(1)构造函数不能,因为构造函数要修改类的成员变量

const修饰函数表示该函数的返回值是const类型的,该返回值只能赋值给同类型的const变量。

const是可以修饰类的成员函数,但是该函数不能修改数据成员。构造函数也属于类的成员函数,但是构造函数是要修改类的成员变量,所以类的构造函数不能申明成const类型的。

(2)静态成员函数不行

static静态成员是属于类的,而不属于某个具体的对象,所有的对象共用static成员。this指针是某个具体对象的地址,因此static成员函数没有this指针。而函数中的const其实就是用来修饰this指针的,意味this指向的内容不可变,所以const不能用来修饰static成员函数

问:对于类中的普通成员函数,都可以将其申明为const函数吗。

个人理解:对于那些要修改数据成员的函数,不能将其定义为const函数

5.虚函数表

为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。

每个包含了虚函数的类都包含一个虚表。

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

类A的虚表如下图所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MNGoqLEx-1638198792302)(C:\Users\Janey\AppData\Roaming\Typora\typora-user-images\image-20211027224256845.png)]

虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pb92lYRT-1638198792307)(C:\Users\Janey\AppData\Roaming\Typora\typora-user-images\image-20211027224438143.png)]

由上图所示,不同对象的虚表指针都指向同一个虚表!!

一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。

5.1动态绑定

C++是如何利用虚表和虚表指针来实现动态绑定的

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};
class B : public A {
public:
    virtual void vfunc1();
    void func1();
private:
    int m_data3;
};
class C: public B {
public:
    virtual void vfunc2();
    void func2();
private:
    int m_data1, m_data4;
};

类A是基类,类B继承类A,类C又继承类B。类A,类B,类C,其对象模型如下图3所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-63UNZXTk-1638198792308)(C:\Users\Janey\AppData\Roaming\Typora\typora-user-images\image-20211028221448462.png)]

由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类A的虚表(A vtbl),类B的虚表(B vtbl),类C的虚表(C vtbl)。类A,类B,类C的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。

类A包括两个虚函数,故A vtbl包含两个指针,分别指向A::vfunc1()A::vfunc2()

类B继承于类A,故类B可以调用类A的函数,但由于类B重写了B::vfunc1()函数,故B vtbl的两个指针分别指向B::vfunc1()A::vfunc2()

类C继承于类B,故类C可以调用类B的函数,但由于类C重写了C::vfunc2()函数,故C vtbl的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()

上图看起来有点复杂,但是只要抓住**“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”**这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。

非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。

下面我们声明一个类A的指针p来指向对象bObject。虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象bObject的虚表指针。bObject的虚表指针指向类B的虚表,所以p可以访问到B vtbl。如图3所示。

int main() 
{
    B bObject;
    A *p = & bObject;
}

当我们使用p来调用vfunc1()函数时,会发生什么现象?

int main() 
{
    B bObject;
    A *p = & bObject;
    p->vfunc1();
}

程序在执行p->vfunc1()时,会发现p是个指针,且调用的函数是虚函数,接下来便会进行以下的步骤。

首先,根据虚表指针p->__vptr来访问对象bObject对应的虚表。虽然指针p是基类A*类型,但是*__vptr也是基类的一部分,所以可以通过p->__vptr可以访问到对象对应的虚表。

然后,在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了,所以可以根据所调用的函数定位到虚表中的对应条目。对于p->vfunc1()的调用,B vtbl的第一项即是vfunc1对应的条目。

最后,根据虚表中找到的函数指针,调用函数。从上图可以看到,B vtbl的第一项指向B::vfunc1(),所以p->vfunc1()实质会调用B::vfunc1()函数。

5.2 虚函数覆盖虚函数

虚函数是被子类同名函数所覆盖的,如下代码B继承于A,B的f()函数也是虚函数所以它覆盖A的同时也会被它子类覆盖。

class A
{
public:
    virtual void f()
    {
       cout<<"A"<<endl; 
    };
};
class B:public A
{
public:
    virtual void f()
    {
       cout<<"B"<<endl; 
    };
};

int main()
{
    A* pa = new A();
    pa->f();//A
	B*pb = (B*)pa;
    pb->f();//A
    delete pa,pb;
    pa = new B();//多态情况下就看new出来的对象是谁就调谁的函数;
    pa->f();//B
    pb = (B*) pa;
    pb->f();//B
}
输出就是A A B B

6.浅拷贝与深拷贝

浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。

但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

拷贝者和被拷贝者若是同一个地址,则为浅拷贝,反之为深拷贝。

经过测试,类对象的拷贝构造函数不能称为完全意义上的深拷贝,看网上帖子说是浅拷贝,实际上应该叫位拷贝,按字节拷贝成一样(网上说的浅拷贝指的是位拷贝,特指如果类成员变量有指针的话,你可以发现指针指向的地方是一样的,而不是真正意义上的深拷贝新开一块内存给指针用)

7.多态

多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。

C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。

class Shape 
{
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      virtual int area()
      {
         cout << "Parent class area :" <<endl;
         return 0;
      }
};

class Rectangle: public Shape{
   public:
      Rectangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Rectangle class area :" <<endl;
         return (width * height); 
      }};
class Triangle: public Shape{
   public:
      Triangle( int a=0, int b=0):Shape(a, b) { }
      int area ()
      { 
         cout << "Triangle class area :" <<endl;
         return (width * height / 2); 
      }};
// 程序的主函数
int main( ){
   Shape *shape;
   Rectangle rec(10,7);
   Triangle  tri(10,5);

   // 存储矩形的地址
   shape = &rec;
   // 调用矩形的求面积函数 area
   shape->area();

   // 存储三角形的地址
   shape = &tri;
   // 调用三角形的求面积函数 area
   shape->area();
}

编译器看的是指针shape的内容,而不是它的类型。因此,由于 tri 和 rec 类的对象的地址存储在 *shape 中,所以会调用各自的 area() 函数。

正如所看到的,每个子类都有一个函数 area() 的独立实现。这就是多态的一般使用方式。有了多态,您可以有多个不同的类,都带有同一个名称但具有不同实现的函数,函数的参数甚至可以是相同的。

特别注意上面引入了虚函数的概念

虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。

我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定

纯虚函数

您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。

我们可以把基类中的虚函数 area() 改写如下:

class Shape {
   protected:
      int width, height;
   public:
      Shape( int a=0, int b=0)
      {
         width = a;
         height = b;
      }
      // pure virtual function
      virtual int area() = 0;
};

8.C++ 接口(抽象类)

接口描述了类的行为和功能,而不需要完成类的特定实现。

C++ 接口是使用抽象类来实现的,抽象类与数据抽象互不混淆,数据抽象是一个把实现细节与相关的数据分离开的概念。

如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的,如下所示:

class Box{
   public:
      // 纯虚函数
      virtual double getVolume() = 0;
   private:
      double length;      // 长度
      double breadth;     // 宽度
      double height;      // 高度
};

设计抽象类(通常称为 ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,它只能作为接口使用。如果试图实例化一个抽象类的对象,会导致编译错误。

因此,如果一个 ABC 的子类需要被实例化,则必须实现每个虚函数,这也意味着 C++ 支持使用 ABC 声明接口。如果没有在派生类中重载纯虚函数,就尝试实例化该类的对象,会导致编译错误。

可用于实例化对象的类被称为具体类

9.数组指针和指针数组

int *p1[5];  //指针数组
int (*p2)[5];//数组指针

int * p1[5],因为“[]”的优先级要比“ * ”要高,p1 先与“[]”结合,构成一个数组的定义,数组名为 p1,而“int”修饰的是数组的内容,即数组的每个元素。也就是说,该数组包含 5 个指向 int 类型数据的指针

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EAKn0Qlu-1638198792309)(C:\Users\Janey\AppData\Roaming\Typora\typora-user-images\image-20211105212010987.png)]

“int(*p2)[5]”,“()”的优先级比“[]”高,“ * ”号和 p2 构成一个指针的定义,指针变量名为 p2,而 int 修饰的是数组的内容,即数组的每个元素。也就是说,p2 是一个指针,它指向一个包含 5 个 int 类型数据的数组,如图 2 所示。很显然,它是一个数组指针,数组在这里并没有名字,是个匿名数组。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RQIARML4-1638198792310)(C:\Users\Janey\AppData\Roaming\Typora\typora-user-images\image-20211105212126480.png)]

delete a; //仅释放了a指针指向的全部内存空间 但是只调用了a[0]对象的析构函数 剩下的从a[1]到a[9]这9个用户自行分配的m_cBuffer对应内存空间将不能释放 从而造成内存泄漏
delete [] a; //调用使用类对象的析构函数释放用户自己分配内存空间并且 释放了a指针指向的全部内存空间

注意如果数组元素是基本类型,那么delete a和delete [] a释放空间是等价的。如果a是自定义类型,那么delete a只会执行数组中第一个元素的析构函数。

class Babe
{
public:
    Babe()
    {
        cout << "Create a Babe to talk with me" << endl;
    }
    ~Babe()
    {
        cout << "Babe don\'t go away,listen to me" << endl;
    }
};
//////////main function
int main()
{
    Babe* pbabe = new Babe[3];
	delete pbabe;//这里提示堆错误,感觉是因为资源没释放干净导致的
	Babe* pbabe_b = new Babe[3];
	delete []pbabe_b;
    return 0;
}

输出:

delete pbabe;的输出
Create a Babe to talk with me
Create a Babe to talk with me
Create a Babe to talk with me
Babe don't go away,listen to me  
    
delete []pbabe_b;的输出如下:
Create a Babe to talk with me
Create a Babe to talk with me
Create a Babe to talk with me
Babe don't go away,listen to me
Babe don't go away,listen to me
Babe don't go away,listen to me

10 继承情况下类的构造函数调用

构造函数是从最初始的基类开始构造的,各个类的同名变量没有形成覆盖!都是单独的变量。

10.1 就近调用原则

如果父辈存在相关接口则优先调用父辈接口,如果父辈也不存在相关接口就调用祖父辈接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值