C++复习大全(各种知识点)

前言

这篇博客是我之前的一个礼拜复习总结的各种知识点,可能有些多,其中的一些观点是来自于《Effective C++》和《C++编程思想》,这两本书中的知识给了我很多启发,也让我懂得了许多不一样的知识点,我连带我的认识以及理解整理起来,希望会对你们有所帮助。

资源就是一旦被使用,将来必须要返还给系统。在c++中最常使用的资源就是动态分配内存(如果分配了内存却从来不归还它,会导致内存泄漏

其他的常见资源还有 文件描述器,互斥锁,图形界面中的字型和笔刷,数据库连接,以及网络 sockets

条款 13 以对象管理资源
  • 资源的分配可能会来不及释放,比如说下面这种情况
void f()
{
    Investment* pInv = createInvestment();
    .......  //这里看起来没有任何问题,但是如果有一个return 语句在这里提前中断,控制流就无法接触到delete语句

    delete pInv;
}
-2 类似的情况还可能发生在循环内,如果循环内有一个 continue或者 goto 语句导致过早退出

-3 还有一种情况就是可能在 ... 中可能抛出异常,

无论delete是如何被忽略掉的,我们泄漏的不只是内含投资对象的那块内存,还包括那些投资对象所保存的任何资源

为了确保对象返回的资源总是可以被释放掉,我们必须把资源放进对象内,当控制流离开函数 f()时,该对象的析构函数会自动的释放那些资源

  • 智能指针 auto_ptr
    智能指针的功能就是其析构函数自动对齐所指对象调用 delete
void f()
{
     std::auto_ptr<Investment>pInv(createInvestment());
                             //调用factory函数
                             //一如以往地使用pInv
                             //经由 auto_ptr 的析构函数自动删除pInv
}
  • 获得资源后立刻放进管理对象
  • 管理对象运用析构函数确保资源被释放
  • (如果在释放资源过程中抛出异常,那么可以在析构函数内部实现资源释放)
  • 需要注意的一点是,由于 auto_ptr 被消耗时会自动删除它所指的对象,所以一定要注意别让多个 auto_ptr 指向同一个对象,这样可能会造成内存泄漏,因此只能有一个指针具有管理权
std::auto_ptr<Investment> 
  pInv1(createInvestment()); //pInv1 指向返回物

  std::auto_ptr<Investment> pInv2(pInv1); //pInv2指向对象,pInv1 设为NULL

  pInv1 = pInv2; //pInv1 指向对象,pInv2 设为NULL
  • auto_ptr 的代替方案 是增加了引用计数的智能指针,用来持续追踪共有多少个对象指向某笔资源,并在无人指向它时自动删除该资源 。RCSP 提供的行为类似于垃圾回收,但是不同的是RCSP无法打破环状引用,比如两个已经没有被使用的对象彼此互指,因而好像还处在被使用状态。(这块需要深入了解)

boost::scoped_array 和 boost::shared_array classes

条款 14 在资源管理类中小心copying 行为
  • 并非所有的资源都是heap-based ,对那种资源而言,像 auto_ptr 和tr1::shared_ptr 这样的智能指针往往不适合作为资源掌管者,因此有时候我们需要自己建立资源管理类
  • 假设我们使用 C API 函数处理类型为 Mutex 的互斥器对象,共有lock 和unlock两种函数可用
void lock(Mutex* pm); //锁定pm所指向的互斥器
void unlock(Mutex* pm)//将互斥器解除锁定

-建立一个类来管理机锁 (资源在构造期间获得,字析构期间释放)

class Lock{
    public:
    explicit Lock(Mutex* pm)
    :mutexPtr(pm)
    {
          lock(mutxPtr);
    }
    ~Lock()
    {
        unlock(mutexPtr);
    }
    private:
    Mutex *mutexPtr;
};
  • 当一个RAII对象被复制,会发生什么?
    (1)禁止复制 把拷贝函数定义为私有的
    (2)对底层资源祭出“引用计数法”
    (3)复制底部资源(深拷贝)在你不再使用时记得释放
    ($)转移底部资源的所有权

请记住

复制RAII对象必须一并复制它所管理的资源,所以资源的拷贝行为决定了RAII对象的拷贝行为
普遍儿常见的RAII class copying 行为是:抑制拷贝行为,施行引用计数法

条款 16 : 成对使用 new 和delete 时要采取相同形式

std::string * stringArray = new std::string[100];

delete stirngArray;

stringArray所包含的一百个对象中99个没有被析构函数释放掉
  • 当你使用new 时,会发生两件事 1.内存被分配 2.针对此内存会有一个或者多个构造函数被调用

  • delete必须知道内存中有多少个对象,才能去相应的调用多少次析构函数

单一对象和数组的内存布局决定了析构方式的差别(具体看图)
  • 如果你使用delete时加上[],delete便认定指针指向一个数组,否则它就认定指针指向一个单一对象。
  • 基于这个原因,我们必须要将 new 和delete 匹配起来使用,这样才能避免内存泄漏

宁以pass-by-reference-to-const替换pass-by-value

在缺省的情况下,C++是以传值的方式传递对象到函数,除非我们自己指定,否则函数参数都是以实际参数的一份拷贝作为初值。这些拷贝的复件都是通过调用拷贝构造函数产生的,这样做使得传值调用的方式带来了很大的时间开销和内存开销(当然,这是基于一个比较大的对象来说的)

- 每次给函数传参,都会调用一次拷贝构造函数,同样的,也会调用一次析构函数

class Person
{
    public:
    Person();
    virtual ~Peson;

    private:
    std::string name;
    std::string address;
};

class Student:public Person{
    public:
       Student();
       ~Student();

    private:
       std::string schoolName;
       std::string schoolAddress;
};

(1)Student plato;
(2)Student s(plato);

现在我们来分析一下这段代码,先提前说一下,这段代码的拷贝构造函数调用次数一定会让你感到很震惊。

  • 单就拿代码(2)来说吧。已经存在一个 Student 类对象plato,然后我们要拷贝构造一个 Studnet 类对象 s。由于s是拷贝构造 plato 的,那就会调用一次派生类拷贝构造,而派生类又会调用基类的拷贝构造,这就已经是两次了,对应的,既然调用了两次拷贝构造,那就会调用两次析构函数。
  • 然后再分析,派生类中的对象继承了基类的对象,所以总共有四个string 对象,那么又会调用四次构造函数,对应四次析构函数
  • 最后得出结论,代码(2)总共调用了六次构造函数和六次析构函数

  • 但是,如果传递的是引用的话,那就可以直接对对象进行操作,避免调用构造函数和析构函数。因为没有任何新的对象被建立,以引用传递也可以避免对象切割问题,当一个派生类以值传递的方式将会被声明为基类对象,基类的拷贝构造函数被调用,造成派生类的特化性质全被切割

  • 为了解决切割问题,我们可以给函数的参数传入一个 const 的引用

  • 引用的底层实际上也就是个指针
  • 但是对于STL的迭代器和函数对象以及内置类型,传值调用更加适合

条款21 必须返回对象时,别妄想返回引用

class rational{
    public:
    Rational(int numrator =0,
             int denominator = 1);
    private:
       int n,d;
       friend const Rational operator* (const Rational &lhs,
                                        const Rational &rhs);
    };
}

我们需要搞清楚的一个问题是,引用既然是变量的别名,那就必须有一个变量存在,这个时候,如果我们的函数没有定义变量而直接就用引用,那么这个引用一定是存在问题的。这个时候,我们或许可以想到使用在函数中直接定义一个局部变量,然后有一个引用作为他的别名。但是我们需要考虑的问题是,当函数的生命周期结束,这个开辟在栈上的局部变量一定是要被销毁的。这个时候,我们的引用仍然指向这块变量,殊不知,这块变量早已经消失了,那么引用也就失去了它的价值。

  • 基于以上观点,我们一定要注意,不要对一个局部变量声明一个引用,这样一定会引发问题。

-为了解决这个问题,我们只能采取另一种方案,即直接在堆上动态开辟内存空间给对象。这样做是可以避免函数栈桢自动销毁的问题,但是,还有另一个问题有待解决,这是什么呢?看一段代码

const Rational& operator* (const Rational& lhs,
                           const Rational& rhs)
    {
        Rational* result = new Rational(lhs.n * rhs.n, lhs.d * rhs.d);
        return *result;
    }
    即使如此,我们还是需要付出代价,因未分配所得的内存将会以一个适当的构造函数进行初始化,既然 new 了一个对象,那么谁来对它进行 delete呢?

//这种情况下,如何 delete?
Rational w,x,y,z;
w = x*y*z;   //与operator*(operator*(x,y),z) 相同

在上面的代码中,同一个语句调用了两次 operator* 因而使用了两次 new ,那么相对的也就需要两次 delete.但却没有合理的办法让 operator* 使用者进行那些delete调用,因为没有合理的办法让他们取得 operator* 返回的引用背后隐藏的那个指针,这样做绝对会导致资源泄漏。

请记住

  • 绝对不要返回一个指针或者引用指向一个 local static 对象而有可能同时需要多个这样的对象。

条款 23:宁以 non-member,non-friend 替换 member 函数

  • 面向对象守则要求,数据以及数据操作的那些函数应该被捆绑在一起,这意味着它建议member函数是较好的选择,但是很不幸的是这个建议不正确。

  • C++ 引用简介

  • 引用就是变量的别名,引用的基本用法和指针是相同的,但是带引用的函数调用比带指针的函数调用在语法构成上更加清晰。
  • 引用看起来像是按值传递,实际上是按地址传递。
  • 如果指针声明为 void* ,它意味着任何类型的地址都可以间接引用那个指针
int main()
{
    void* vp;
    char c;
    int i;
    float f;
    double d;

    vp = &c;
    vp = &i;
    vp = &f;
    vp = &d;
}

- 但是一旦我们间接引用一个 void*,就会丢失关于类型的信息,这意味着在使用前必须转化为正确的类型

int main()
{
    int i=99;
    void* vp =&i;
    必须先转换类型
    *((int*)vp) = 3;
}
//但是这样做也会存在一个问题,既然可以转化为 int 类型,那么同样的也就可以转化为 char ,double,这将改变已经分配给int 的存储空间大小,可能会引起程序崩溃

作用域

  • 作用域就是告诉我们一个变量的有效范围,它在哪里创建,在哪里销毁。
  • 变量的有效作用域从它的定义点开始,到和定义变量之前最邻近的的开括号配对的第一个闭括号
  • C语言强制在作用域的开始处就定义所有的变量,以便编译器创建一个块时,能给所有这些变量分配空间。
  • C++允许在作用域内的任意地方定义变量,所以可以在正好使用它之前定义,可以在定义变量时对它初始化以防止某种类型的错误

指定存储空间分配

1.全局变量

全局变量是在所有的函数体的外部定义的,程序的所有部分(甚至其他文件中的代码)都可以使用
- 使用extern 可以进行外部链接,使得另一个代码可以使用本代码中的变量。

2.局部变量

局部变量经常被称为自动变量。因为他们在进入作用域时自动生成,离开作用域时自动消失。局部变量默认为auto,没必要显式声明。

  • 寄存器变量
    寄存器变量也是一个局部变量,关键字是 register ,它告诉编译器要尽快访问这个变量,但是这个动作通常是编译器做的,现在许多的编译器会对经常访问的变量直接放在寄存器中,我们如果强制这样声明并没有什么好处。
  • 使用 register 变量,我们不能得到或者计算 register 的地址,register 变量只能在一个块中声明,寄存器变量可以做函数形参

  • 静态变量
    通常情况下,我们如果在函数中定义了一个局部变量,会在函数作用域结束时自动消失,当我们下一次调用这个函数时,会重新创建该变量的存储空间,它的值也会被重新初始化。如果想要时局部变量的值在整个程序都会存在,我们可以定义函数的局部变量为static,并给他一个初值

#include<isotream>
using namespace std;

void func() {
    static int i = 0;
    cout<<"i = "<<++i<<endl;
    }

int main()
{
    for(int x=0;x<10;x++)
    {
        func();
    }
}
}
//如果声明的不是static,那么每次都会打印出来1
static变量在作用域外不可用
3.7 转换运算符

简单的类型转换确实很奏效,但是有时候却会占用更大的内存空间,这可能会破坏其他的数据,因为它强迫编译器把一个数据看做是一个比它实际上更大的类型。这种强制类型转换通常用在指针的类型转换上,因为指针的大小在系统下都是固定的,但是有时候也会存在问题。

  • 把一个整型指针强转为一个long类型的指针,那么编译器就会默认指针指向的是一块long类型的地址,这可能会造成数据冗余,改为short则有可能会造成数据丢失。
C++的显示类型转换
  • static_cast //用于良性和适度良性转换,包括不用强制转换(自动类型转换)- -
  • const——cast //对“const” 和/或“volatile“进行转换
  • reinterpret_cast //转换为完全不同的意思,为了安全使用它,关键必须转化为原来的类型。转换成的类型一般只能用于位操作,否则就是为了其他隐藏的目的。这是所有转化中最危险的。
  • dynamic_cast //用于类型安全的向下转换
静态转换(static_cast)

转换类型包括典型的非强制转换,窄化(有信息丢失)变换,使用void*的强制变换,隐式类型转换和类层次的静态定位

    -
void func(int)
{}
int main(){   //使用static_cast 提升数据类型或者降低数据类型都是可以的
    int i = 0x7fff;  //但是一定要注意数据丢失
    long l;
    float f;
    l=i;
    f=i;
    l = static_cast<long>(i);
    f = static_cast<float>(i);
}
常量转换(const_cast)

如果从const 转化为非 const 或从 volatile 转换为非 volatile ,可以使用 const_cast ,这是const_cast 唯一允许的转换

int main()
{
    const int i = 0;
    int *j = (int*)& i;
    j = const_cast<int*>(& i);

    volatile int k = 0;
    int* u = const_cast<int*>(& k);

记住,如果取得了const 的地址,就可以生成一个指向 const 的指针,不用转换是不能将它赋给非 const指针的。
}
重解释转换 (reinterpret_cast) [最不安全的类型转换]

创建复合类型

用typedef命名别名
typedef 原类型名  别名

typedef unsigned long ulong 
  • 在一些重要的场合,编译器必须知道我们正在将名字当做类型处理,所以typedef 起了关键的作用

- typedef 经常用到的地方是指针类型

int* x,y;

typedef int* IntPtr
IntPtr x,y; //生成两个指针

结构体

typedef struct Structure3{
    char c;
    int i;
    float f;
}Struture3;

int main()
{
    Structure3 s1,s2;
    Structure3 *p = &s1;
    p-> c ='a';
}
枚举,枚举本质上就是一个整数,但是他又不完全等价于一个整数。比如一个color的枚举类型,编译器是这样做的

enum color{
a++; //本质上这样是不对的
}; //必须加上;

  • 1.将枚举的值隐式地从 color 强制转化为 int,然后递增该值,再把int强制转化回 color。如果相对color进行增量运算,应该声明一个类。

用union节省空间

  • union 把所有的数据放在一个单独的空间内,他计算出放在union中的最大项所必需的空间数,并生成union的大小
  • 我们创建的是一个能容纳任何union变量的超变量。所有的union变量的地址都是一样的。

- 每次只能取到一个变量的值,因为他们是共用这块空间

数组
void func1(int a[],int size);
void func2(int *a ,int size);
atoi() atol() atof()


int main(int argc,char* argv[])
{
    for(int i=1;i<argc;i++)
    {
        cout<<atoi(argv[i])<<endl;

    }
}

把变量和表达式转化为字符串

define PR(x) cout<< #x “=” << x <<”\n”;

3.10 函数地址

  • 一旦函数被编译并载入计算机中执行,它就会占用一块内存,这块内存有一个地址,因此函数也有地址。
  • 可以通过指针使用函数地址,就像可以使用变量的地址一样。
定义函数指针
要定义一个无参无返回值的函数
void (*funcptr)(); //记得函数指针的分辨
如果这样声明,就不一样了
void *funcPtr(); //不加()就会看成是一个返回值为 void* 的函数
复杂的声明和定义
void*(*(*fp1)(int))[10]; // fp1 是一个指向函数的指针,该函数接受一个整形参数并返回一个指向 10 个void  指针数组的指针

float(*(*fp2)(int,int,float))(int); //fp2是一个指向函数的指针,该函数接收三个参数且返回一个指向函数的指针,该函数

typedef double (*(*(*fp3)())[10])();

int (*(*f4())[10])();

12.6 动态特性

  • 在多数情况下,程序的功能是在编译时就确定下来的,我们称为静态特性,如果程序的功能是在运行时才确定下来的,则称为动态特性。
  • C++虚函数,抽象基类,动态绑定和多态构成了出色的动态特性。

虚函数

如果一个类的一个函数被声明为虚函数,那么其派生类的对应函数也自动成为虚函数,这样一级级传递下去。虽然默认是虚函数,但是我们最好还是显式地声明一下,方便我们理解。

class Shape{   //虚函数的重写
    public:
    virtual void Draw(void);
    };

class Rectangle;public Shape{
    public:
    virtual void Draw(void);
}

抽象基类

不能实例化出对象的类称为抽象类(那些把所有的构造函数都声明为private的类也是不能实例化的类)
- 抽象类的唯一目的就是让其派生类继承并实现它的接口方法。
- 如果该基类的虚函数声明为纯虚函数,那么该类就被定义为抽象基类。纯函数虚函数是在声明时将其初始化为0的函数

class Shape{
    public:
       virtual void Draw(void) = 0;
};

分析:函数名就是函数的地址,将一个函数初始化为0意味着函数的地址将为0,这就是在告诉编译器,不要为该函数编地址,从而阻止该类的实例化行为。
- 抽象基类的主要用途是:(接口与实现分离):不仅要把数据成员隐藏起来,而且还要把实现完全隐藏起来,只留一些接口给外部调用。即使将来实现改变了,接口仍然可以保持不变。
- 一般的信息隐藏就是把类的所有数据成员声明为private 或者protected,并提供相应的get 和set来访问对象的数据。抽象基类则进一步,它把数据和函数实现都隐藏在实现类中,而在抽象基类中提供丰富的接口函数供调用,这些函数都是public的纯虚函数,这样的抽象基类叫做接口类。

class IRectangle
{
    virtual ~IRectangle(){}
    virtual float Getlength()const  = 0;
    virtual void Setlength(float newLength) = 0;
    virtual float Getwidth()const = 0;
    virtual void Setwidth(float newWidth) = 0;
    static IRectangle*_stdcall CreateRectangle(); //入口函数
    void Destroy(){ delete this;}
};

class RectangleImp:public TRectangle{
    public:
         RectangleImp():m_length(1),m_width(1),m_color(0x00FFEC4D){}
         virtual ~RectangleImp(){}
         virtual float Getlength()const  {  return m_length;}
         virtual void Setlength(float newLength) {  m_length = newLength; }
    private:
        float m_length;
        float m_width;
        RGB   m_color;
};
  • 由于抽象基类不能被实例化,并且实现类被完全隐藏,所以必须以其他的途径使用户能够获得实现类的对象,比如提供入口函数来动态创建实现类的对象。入口函数可以使全局函数,但最好是静态函数。
IRectangle* _stdcall IRectangle::CreateRectangle()
{
    return new(nothrow)RectangleImp;
}
void main()
{
    IRectangle *pRect = IRectangle::CreateRectangle();

}
动态绑定
  • C++ 的动态绑定机制是如何实现的?
  • 程序之所以能够在运行时选择正确的虚函数,必定隐藏了一段运行时进行对象类型判断或是函数寻址的代码。
  • C++编译器必须为每一个多态类至少创建一个虚函数表,它其实就是一个函数指针数组,其中存放着这个类所有的虚函数的地址及该类的类型信息,其中也包括那些继承但未改写的虚函数。
  • 同类对象的类型信息完全相同,所以只需要在该类的vtable中保留一份就足够了。每一个多态对象都有一个隐含的指针成员,它指向所属的通过基类指针或引用对虚函数的调用语句都会被编译器改写成下面这种形式
(*(p->vptr[slotNum]))(p,arg-list); //指针当做数组来用,最后改写为指针运算
派生类定义中的名字(对象或函数名)将义无反顾地遮蔽(隐藏)基类中任何同名的对象或函数
  • 函数原型完全相同,当返回类型不同时称为协变

运行时多态

当许多的派生类因为继承了共同的基类而建立 is -a 关系时,没一个派生类的对象都可以被当成基类的对象来使用,这些派生类对象能对同一个函数调用做出不同的反应,这就是运行时多态。

void Draw(Shape *pShape)
{
    pShape->Draw();
}
main()
{
    Circle aCircle;
    Cube   aCube;
    Sphere aSphere;
    ::Draw(&aCircle);
    ::Draw(&aCube);
    ::Draw(&aSphere);
}

关于多态的总结

  • 经过隐含的转型操作,令一个public多态基类的指针或者引用指向它的一个派生类的对象。
  • 通过这个指针或者引用调用基类的虚函数,包括通过指针的反引用调用虚函数,因为反引用一个指针将返回所指对象的引用
  • 使用dynamic_cast<>和typeid运算符

优点

  • 应用程序不必为每一个派生类编写功能调用,只需要对基类的虚函数进行改写或扩展即可。可以大大提高程序的可复用性和可扩展性。
  • 派生类的功能可以被基类指针引用,这叫向后兼容。以前写的程序可以被将来写的程序调用,这不足为奇,但是将来写的程序可以被以前写的程序调用那就很了不起了。

多态数组

在基类对象数组中存放派生类对象

Shape      a(Point(1,1));
Circle     b(Point(2,2),5);
Rectangle  c(Point(3,3),Point(4,4));
Shape   myShapes[3];
myShapes[0] = a;
myShapes[1] = b;
myShapes[2] = c;

for(int i=0;i<3;i++)
  myShapes[i].Draw();

C++对象模型
- 非静态数据成员被放在每一个对象体内作为对象专有的数据成员
- 静态数据成员被提取出来放在程序的静态数据区内为该类所有对象共享,因此仅存在一份。
- 静态和非静态成员函数最终都被提取出来放在程序的代码段中并为该类的所有对象共享,因此每一个成员函数也只存在一份代码实体。

因此,构成对象本身的只有数据,任何成员函数都不隶属于任何一个对象,非静态成员函数与对象的关系就是绑定,绑定的中介就是this指针。
增加了继承和虚函数的类的对象模型变得更加复杂,规则如下:
  • 为每一个多态类创建一个虚函数指针数组vtable,该类的所有虚函数(继承自基类或者新增的)的地址都保存在这张表中。
  • 如果基类已经插入了vfptr,则派生类将继承和重用该vfptr
  • 如果派生类从多个基类继承或者有多个继承分支,而其中若干个继承分支上出现了多态类,则派生类将从这些分支中的每个分支上继承一个vfptr,编译器也将为它生成多个vtable
  • vfptr在派生类对象中的相对位置不会随着继承层次的逐渐加深而改变,现在的编译器一般都将vfptr放在所有数据成员的最前面。
  • 只有虚函数访问需要经过vfptr的间接寻址,增加了一层间接性,因此带来了一些额外的运行时开销

隐含成员

  • 一个C++的复合类型对象,其可能的隐含成员包含:若干vfptr,默认构造函数,默认拷贝构造函数,析构函数和默认拷贝赋值函数
  • 该类含义虚函数,无论是自己定义的还是从基类继承下来的。
  • 该类的继承链中至少有一个基类是多态类
  • 该类至少有一个虚基类
  • 该类包含了多态的成员对象,但是该类不一定是多态类
显然,当创建一个对象的时候,其隐含的成员vfptr必须被初始化为指向正确的vtable,而且这个初始化工作只能在运行时完成,所以这个任务自然就交给了构造函数。
C++对象模型要充分考虑对象数据成员的空间效率和访问速度,以优化性能。另外,每一个对象必须占据足够大的内存空间以便容纳其所有的非静态数据成员。因此,对象的实际大小可能比简单地把各个成员的大小加在一起的结果还要大。
造成这种结果的原因主要有两条:
  • (1)由编译器自动安插的额外隐含数据成员,以支持对象模型,入vfptr
  • (2)除去对存取效率的考虑而增加的填补字节,以使对象的边界能够对齐到机器字长(WORD),即为WORD的整数倍
C++编译器如何处理成员函数
  • 在编译器眼中,同一个函数只存在一个实现,不管是全局函数还是成员函数。对于在两个编译单元中分别定义的两个完全相同的static全局函数,由于编译器认为它们是不同的函数,因此会分别为它们生成可执行代码。
  • C++通过命名技术把每一个成员函数都转换成了名字唯一的全局函数,并把通过对象,指针和引用对每一个成员函数的调用语句改写成相应的全局函数调用语句。
  • 需要了解的是,不同的C++编译器对class的数据成员,成员函数和全局函数等的命名方案是不同的,这是造成不同编译器之间存在二进制连接兼容性的主要原因之一
C++如何处理静态成员
  • 在C++中,凡是使用static关键字声明和定义的程序元素,不论其作用域是文件,函数或是类,都将具有static存储类型,并且其生存期限为永久,即在程序开始运行时创建,在程序结束时销毁。因此,类的静态成员在本质上就是一种全局变量或函数。
  • 类的静态数据成员可以在class的定义中直接初始化,但是要清楚:这只是声明并给它提供一个初值而已,还必须在某一个编译单元把它定义一次(分配内存)
  • 静态成员函数像其他成员函数一样,也要经过名字修饰处理并被提出到class之外,但是不同的是它们不需要this指针参数
  • 基类的静态成员也会被派生类继承,但这种继承并不是继承它们的实体,而是使得它们能在派生类中直接访问。
  • 静态成员的最大特点是没有this指针,因此可以通过作用域解析运算符直接引用。

类型

  • C++ 强制类型转换相比较C语言能够更好一点,它对用户进行的操作提醒,有可能产生什么样的后果,但是C语言就是一把转,不太适合

相近类型支持隐式类型转换
不相关类型一定是强制类型转换

static_cast 类型转换

对相关类型或者相近类型隐式类型转换

double d = static_cast<int>(i);

int *p = &i;
int j = reinterptret

int j = reinterpret_cast<int>(p)  //不相关类型的转换,部分强制类型的转换

const int*p1 = p; //提醒把 const 属性去掉了
int* p2 =const_cast<int*>(p1)//去const属性--部分强制类型转换
typedef void (*Func)()

int Dosomething(int i)
{
    cout<<"Do something"<<endl;
    }
void Test()
{
    Func f =reinterpret_cast<Fun>(Dosomething);
}
volatile  const int a = 1; //通过查看汇编代码可以确定 a 就是 1 ,无法被修改,放在代码段也就是常量区,这样是一种优化,默认不会被修改,也就不会再内存中去找变量值。

class A{

}

dynamic_cast 只能用于含有虚函数的类转换(把父类指针转化为子类指针)

B* p1 = (B*)p;
B* p2 = dynamic_cast<B*>(p);
  • 可以让我们识别指向的是父类还是指向的子类

如果是父类指针指向子类,那么访问子类元素就存在越界,但是如果是子类指针的,就可以访问到。

  • 可以实现安全的转换,如果是父类要转换为子类不安全,就会返回 0
隐式类型转换-具有单参数构造函数的类型
  • exciplit
class A{
    public:
      A(int a);
        :_a(a)
        {
            cout<<"build"<<endl;
        }
}

int main()
{
     A a(10);
     a b = 20; //构造函数和拷贝构造函数同时使用时,可以生成一个中间临时变量直接优化代码(生成匿名对象)在一个表达式里面就会优化

}

-在C++中,初始化和清楚地概念是简化库的使用的关键之处,并可以减少那些在客户程序员忘记去完成这些操作时会引发的细微错误

用构造函数确保初始化

如果一个类有构造函数,那么编译器在创建对象时就自动调用这个函数。
- 成员函数默认传的第一个参数是 this 指针,所以构造函数传入的第一个参数是 this 指针,也就是调用这一函数的对象的地址,对构造函数来说,this 指针指向一个没有被初始化的内存块,构造函数的作用就是正确的初始化该内存块。
- 构造函数也可以像普通函数一样传递参数,指定对象该如何创建或设定对象初始值

用析构函数确保清除
  • 当对象超出他的作用域时,编译器将自动调用析构函数
清除定义块

出去安全性的考虑,应该尽可能在靠近变量的使用点处定义变量,并在定义时就初始化,通过减少变量在块中的生命周期,就可以减少该变量在块的其他地方被误用的机会,另外,程序的可读性也会增强,因为读者不需要跳到块的开头去确定变量的类型

聚合初始化
  • 当产生一个聚合对象时,要做的只是指定初始值,然后初始化工作就由编译器去承担
struct X{
    int i;
    float f;
    char c;
}

X x1  ={1,2.2,'c'};

当必须指定构造函数调用时,最好这样做
Y y1[] = {Y(1),Y(2),Y(3) };

默认构造函数

默认构造函数就是不带任何参数的构造函数。当编译器需要创建一个对象又不知道任何细节时,默认的构造函数就显得非常重要
- 当有构造函数而没有默认构造函数时,定义的变量就会出现一个编译错误
- 因为由编译器生成的构造函数应该可以做一些智能化的初始化工作,比如把对象的所有内存置零。但是实际上编译器并不会这样做。因为这样做会增加额外的负担,而且使程序员无法控制。
- 解决办法,如果我们还是想要把内存初始化为0,那就得显式地编写默认的默认构造函数。
- 构造函数的重载,当我们想要初始化对象中不同个数的数据时,我们就可以同时在类中声明在类外定义多个构造函数。但是在进行构造函数重载时一定要注意一点:当有全部都有初始值得构造函数时就不要再定义其他的构造函数了,因为这样做会导致构造函数调用不清晰。

第七章【 函数重载与默认参数 】

名字修饰

  • 函数重载是为了解决当多个函数实现的功能相同只是参数有所不同时的做法,这样可以提高代码的复用性,同时也可以使得代码更加简洁,增加了可读性。
  • 函数重载的内部实现机制其实是编译器对函数的名字进行了修饰:通过函数的参数不同而改变函数的名字,这样就可以实现实际调用的是两个不同的函数
  • 一个很有趣的结论->通过返回值重载
    猜想:既然可以通过参数或者范围来实现重载,应该也就可以使用返回值进行重载。
int f();
void f();

当编译器能够从上下文中唯一确定函数的意思时,如 int x = f();,这样当然是可以的,然而,在C语言中总是可以调用一个函数但忽略它的返回值,即调用了函数的副作用

类型安全连接
  • 对名字修饰还可以带来一个好处。在C中如果用户错误的声明了一个函数,或者更糟糕地,一个函数还没声明就调用了,而编译器则安函数被调用的方式去推断函数的声明。

联合

一个联合也可以带有构造函数,析构函数,成员函数甚至访问控制

union U {
    privateint i;
       float f;
    public:
       U(int a);
       U(float b);
       ~U();
       int read_int();
       float read_float();
};
U::U(int a){ i=a;}
U::U(float b) { f =b; }
U::~U() { cout<<"U::~U()\n" }
int U::read_int() { return i; }
float U::read_float() { return  f; }

int main()
{
    U X(12), Y(1.9F);
    cout<<X.read_int()<<endl;
    cout<<Y.read_float()<<endl;

}

常量

const 关键字现在用于各种场景,指针,函数变量,返回类型,类对象以及成员函数。

值替代

define BUFSIZE 100

BUFSIZE 是一个名字,它只是在预处理期间存在,因此它不占用存储空间且能放在一个头文件里,目的是为使用它的所有编译但愿提供一个值。
const int bufsize = 100;
这样就可以在编译时编译器需要知道这个值的任何地方使用bufsize,同时编译器还可以执行常量折叠

头文件的const

通过包含头文件,可把const定义单独放在一个地方并把它分配给一个编译单元,C++中的 const 默认为内部连接,const 仅在const被定义过的文件里才是可见的,而在连接时不能被其他编译单元看到。
extern const int bufsize ;
编译器并不会为const 创建存储空间,相反它把这个定义保存在符号表中。但是,extern 强制进行了存储空间分配,由于 extern 意味着外部连接,因此必须分配存储空间

常量折叠

当众多的const 在多个cpp 文件中分配内存,容易引起连接错误,然而,const 默认内部连接,所以连接程序不会跨过编译单元连接那些定义,因此不会有冲突。在大部分场合使用内建数据类型的情况,包括常量表达式,编译都能执行常量折叠

const 的安全性

如果不想让一个值改变,就应该声明成const,这不仅可以防止意外的更改提供安全措施,也消除了读存储器和读内存操作,使编译器产生的代码更有效。

C与C++中const的区别
  • 在C语言中,const只是被定义为一个不能被修改的普通变量,因此会分配存储空间,而在C++中 const是被看做一个编译时常量,不会分配内存
  • C语言中,const默认是外部连接,因此不存在内存折叠的问题,在C++中,const 默认为内部连接,可以完成
指向const的指针

const int* u;
int const* u;
这俩其实是一样的,都是一个指向常量的指针,因此指向的常量不能被修改

指向普通变量的常指针

int const* u;
指针的指向在它的生命周期里不能被修改

指向const的常指针

const int const* u;
这种情况,不论是指向的变量还是指针本身的指向都不可以被修改。

const权限问题
  • 可以把一个非const对象的地址赋给一个const指针,这样可以达成的效果就是可以使得本来可以修改的变量强制不能被修改掉,这属于权限缩小。
  • 但是不可以把一个const对象赋给一个非const指针,这属于权限扩大,这种操作是不允许的。

关于C++的复习,我会一直坚持下去的,所以这个系列的复习笔记我会时常更新的。因为我是两本书一块看的,所以可能连贯性不是很大,过一段时间我会整理出一张思维导图,让你们对C++的复习有一个全面的概括。最后,感谢你们可以看到这里,希望可以交个朋友,多交流经验。

猜你喜欢

转载自blog.csdn.net/zb1593496558/article/details/80488367