后台开发面试C++(一)

目录

static 关键字的作用

类的大小

Const

常量

指针和常量

 常量与指针(*在const右侧,指向内容不能改,*在const左侧,指针不能指别的)

const修饰函数传入参数

修饰函数返回值

const修饰成员函数(c++特性)

常量与引用

常量函数

C++中指针和引用的区别

1.指针和引用的定义和性质区别:

2.指针和引用作为函数参数进行传递时的区别。

(1)指针作为参数进行传递:

(2)将引用作为函数的参数进行传递。

构造函数为什么不能是虚函数?

析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数?

虚函数和多态

虚函数和纯虚函数

new和malloc的区别

一、new和delete

1.1 规则

1.2 new/new[] 用法

1.3 delete/delete[] 用法

二、new和malloc的区别

2.1 属性

2.2 参数

2.3 返回类型

2.4 自定义类型

2.5 重载

2.6 内存区域

2.7 分配失败

2.8 内存泄漏

重载和重写和重定义

重载(静态多态)

重写(动态多态)

重定义(隐藏)是指派生类的函数屏蔽了与其同名的基类函数

Fork 函数

Fork,wait,exec函数

C++中拷贝赋值函数的形参能否进行值传递

C++智能指针

auto_ptr

unique_ptr 

share_ptr

weak_ptr

c++11新特性

Struct和Class区别

strcut和class的区别

C++类中的三大继承方式:公有继承、私有继承、保护继承

父类子类构造析构顺序


static 关键字的作用

  1. 隐藏。(static函数,static变量均可)
  2. 保持变量内容的持久。(static变量中的记忆功能和全局生存期)
  3. static默认初始化为0(static变量
  4. C++中的类成员声明static(有些地方与以上作用重叠)

  隐藏

当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
举例来说明。同时编译两个源文件,一个是a.c,另一个是main.c。

//a.c
char a = 'A'; // global variable
void msg()
{
     printf("Hello\n");
}
 
//main.c
 
int main()
{
     extern char a; // extern variable must be declared before use
     printf("%c ", a);
     (void)msg();
     return 0;
}

结果

A Hello

 

为什么在a.c中定义的全局变量a和函数msg能在main.c中使用?前面说过,所有未加static前缀的全局变量和函数都具有全局可见性,其它的源文件也能访问。此例中,a是全局变量,msg是函数,并且都没有加static前缀,因此对于另外的源文件main.c是可见的。
如果加了static,就会对其它源文件隐藏。例如在a和msg的定义前加上static,main.c就看不到它们了。利用这一特性可以在不同的文件中定义同名函数和同名变量,而不必担心命名冲突。static可以用作函数和变量的前缀,对于函数来讲,static的作用仅限于隐藏.。


  保持变量内容的持久。(static变量中的记忆功能和全局生存期)

存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围,说到底static还是用来隐藏的。虽然这种用法不常见

PS:如果作为static局部变量在函数内定义,它的生存期为整个源程序,但是其作用域仍与自动变量相同,只能在定义该变量的函数内使用该变量。退出该函数后, 尽管该变量还继续存在,但不能使用它。

#include <stdio.h>
 
int fun()
{
    static int count = 10; //在第一次进入这个函数的时候,变量a被初始化为10!并接着自减1,以后每次进入该函数
    return count--; //a就不会被再次初始化了,仅进行自减1的操作;在static发明前,要达到同样的功能,则只能使用全局变量:    
 
}
 
int count = 1;
 
int main(void)
{
     printf("global\t\tlocal static\n");
     for(; count <= 10; ++count)
               printf("%d\t\t%d\n", count, fun());
     return 0;
}

 

程序的运行结果是:

global  local static

1 10

2 9

3 8

4 7

5 6

6 5

7 4

8 3

9 2

10 1

static默认初始化为0(static变量)

其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。比如初始化一个稀疏矩阵,我们可以一个一个地把所有元素都置0,然后把不是0的几个元素赋值。如果定义成静态的,就省去了一开始置0的操作。再比如要把一个字符数组当字符串来用,但又觉得每次在字符数组末尾加‘\0’;太麻烦。如果把字符串定义成静态的,就省去了这个麻烦,因为那里本来就是‘\0’;

#include <stdio.h>
 
int a;
 
int main()
{
     int i;
     static char str[10];
     printf("integer: %d; string: (begin)%s(end)", a, str);
     return 0;
}

程序的运行结果是:

integer: 0; string: (begin) (end) 

C++中的类成员声明static(有些地方与以上作用重叠)

在类中声明static变量或者函数时,初始化时使用作用域运算符来标明它所属类,因此,静态数据成员是类的成员,而不是对象的成员,这样就出现以下作用:

(1)类的静态成员函数是属于整个类而非类的对象,所以它没有this指针,这就导致了它仅能访问类的静态数据和静态成员函数。      

(2)不能将静态成员函数定义为虚函数。      

(3)由于静态成员声明于类中,操作于其外,所以对其取地址操作,就多少有些特殊 ,变量地址是指向其数据类型的指针 ,函数地址类型是一个“nonmember函数指针”。

(4)由于静态成员函数没有this指针,所以就差不多等同于nonmember函数,结果就 产生了一个意想不到的好处:成为一个callback函数,使得我们得以将C++和C-based X W indow系统结合,同时也成功的应用于线程函数身上。 (这条没遇见过)  

(5)static并没有增加程序的时空开销,相反她还缩短了子类对父类静态成员的访问时间,节省了子类的内存空间。      

(6)静态数据成员在<定义或说明>时前面加关键字static。      

(7)静态数据成员是静态存储的,所以必须对它进行初始化。 (程序员手动初始化,否则编译时一般不会报错,但是在Link时会报错误) 

(8)静态成员初始化与一般数据成员初始化不同:

初始化在类体外进行,而前面不加static,以免与一般静态变量或对象相混淆;
初始化时不加该成员的访问权限控制符private,public等;        
初始化时使用作用域运算符来标明它所属类;
           所以我们得出静态数据成员初始化的格式:
<数据类型><类名>::<静态数据成员名>=<值>

(9)为了防止父类的影响,可以在子类定义一个与父类相同的静态变量,以屏蔽父类的影响。这里有一点需要注意:我们说静态成员为父类和子类共享,但我们有重复定义了静态成员,这会不会引起错误呢?不会,我们的编译器采用了一种绝妙的手法:name-mangling 用以生成唯一的标志。

类的大小

详细解释

class A{};  sizeof(A)=1;
class A{virtual Fun(){}}; sizeof(A)=4(32位机器)/8(64位机器)
class A{static int a;}; sizeof(A)=1;
class A{int a;};  sizeof(A)=4;
class A{static int a;int b;}; sizeof(A)=4;

Const

为什么使用const?采用符号常量写出的代码更容易维护;指针常常是边读边移动,而不是边写边移动;许多函数参数是只读不写的。const最常见用途是作为数组的界和switch分情况标号(也可以用枚举符代替),分类如下:

  常变量:  const 类型说明符 变量名

  常引用:  const 类型说明符 &引用名

  常对象:  类名 const 对象名

  常成员函数:  类名::fun(形参) const

  常数组:  类型说明符 const 数组名[大小]    

  常指针:  const 类型说明符* 指针名 ,类型说明符* const 指针名

首先提示的是:在常变量(const 类型说明符 变量名)、常引用(const 类型说明符 &引用名)、常对象(类名 const 对象名)、 常数组(类型说明符 const 数组名[大小]), const” 与 “类型说明符”或“类名”(其实类名是一种自定义的类型说明符) 的位置可以互换。如:

     const int a=5; 与 int const a=5; 等同

     类名 const 对象名 与 const 类名 对象名 等同

常量

  取代了C中的宏定义,声明时必须进行初始化(!c++类中则不然)。const限制了常量的使用方式,并没有描述常量应该如何分配。如果编译器知道了某const的所有使用,它甚至可以不为该const分配空间。最简单的常见情况就是常量的值在编译时已知,而且不需要分配存储。―《C++ Program Language》
    用const声明的变量虽然增加了分配空间,但是可以保证类型安全。
    C标准中,const定义的常量是全局的,C++中视声明位置而定。

指针和常量

使用指针时涉及到两个对象:该指针本身和被它所指的对象。将一个指针的声明用const“预先固定”将使那个对象而不是使这个指针成为常量。要将指针本身而不是被指对象声明为常量,必须使用声明运算符*const。
    所以出现在 * 之前的const是作为基础类型的一部分:
char *const cp; //到char的const指针
char const *pc1; //到const char的指针
const char *pc2; //到const char的指针(后两个声明是等同的)
    从右向左读的记忆方式:
cp is a const pointer to char. 故pc不能指向别的字符串,但可以修改其指向的字符串的内容
pc2 is a pointer to const char. 故*pc2的内容不可以改变,但pc2可以指向别的字符串

且注意:允许把非 const 对象的地址赋给指向 const 对象的指针,不允许把一个 const 对象的地址赋给一个普通的、非 const 对象的指针。

 常量与指针(*在const右侧,指向内容不能改,*在const左侧,指针不能指别的)

常量与指针放在一起很容易让人迷糊。对于常量指针和指针常量也不是所有的学习C/C++的人都能说清除。例如:

    const int *m1 = new int(10);

    int* const m2 = new int(20);

在上面的两个表达式中,最容易让人迷惑的是const到底是修饰指针还是指针指向的内存区域?其实,只要知道:const只对它左边的东西起作用,唯一的例外就是const本身就是最左边的修饰符,那么它才会对右边的东西起作用。根据这个规则来判断,m1应该是常量指针(即,不能通过m1来修改它所指向的内容。);而m2应该是指针常量(即,不能让m2指向其他的内存模块)。由此可见:

 1. 对于常量指针,不能通过该指针来改变所指的内容。即,下面的操作是错误的:

 int i = 10;

      const int *pi = &i;

      *pi = 100;

  因为你在试图通过pi改变它所指向的内容。但是,并不是说该内存块中的内容不能被修改。我们仍然可以通过其他方式去修改其中的值。例如:

 // 1: 通过i直接修改。

      i = 100;

 // 2: 使用另外一个指针来修改。

      int *p = (int*)pi;

      *p = 100;

 实际上,在将程序载入内存的时候,会有专门的一块内存区域来存放常量。但是,上面的i本身不是常量,是存放在栈或者堆中的。我们仍然可以修改它的值。而pi不能修改指向的值应该说是编译器的一个限制。

 2. 根据上面const的规则,const int *m1 = new int(10);我们也可写作:

  int const *m1 = new int(10);

 3. 在函数参数中指针常量时表示不允许将该指针指向其他内容。

 void func_02(int* const p)

      {

      int *pi = new int(100);

      //错误!P是指针常量。不能对它赋值。

      p = pi;

      }

      int main()

      {

      int* p = new int(10);

      func_02(p);

      delete p;

      return 0;

      }

   4. 在函数参数中使用常量指针时表示在函数中不能改变指针所指向的内容。

  void func(const int *pi)

    {

        //错误!不能通过pi去改变pi所指向的内容!

        *pi = 100;

    }

    int main()

    {

        int* p = new int(10);

        func(p); 

        delete p;

        return 0;

    }

我们可以使用这样的方法来防止函数调用者改变参数的值。但是,这样的限制是有限的,作为参数调用者,我们也不要试图去改变参数中的值。因此,下面的操作是在语法上是正确的,但是可能破坏参数的值:

    #include <iostream>

    #include <string>

    void func(const int *pi)

    {

        //这里相当于重新构建了一个指针,指向相同的内存区域。当然就可以通过该指针修改内存中的值了。

        int* pp = (int*)pi;

        *pp = 100;

    }

    int main()

    {

        using namespace std;

        int* p = new int(10);

        cout << "*p = " << *p << endl;

        func(p);

        cout << "*p = " << *p << endl;

        delete p;

        return 0;

    }

 

const修饰函数传入参数

 将函数传入参数声明为const,以指明使用这种参数仅仅是为了效率的原因,而不是想让调用函数能够修改对象的值。同理,将指针参数声明为const,函数将不修改由这个参数所指的对象。
    通常修饰指针参数和引用参数:
void Fun( const A *in); //修饰指针型传入参数
void Fun(const A &in); //修饰引用型传入参数

修饰函数返回值

可以阻止用户修改返回值。返回值也要相应的付给一个常量或常指针。

 

const修饰成员函数(c++特性)

const对象只能访问const成员函数,而非const对象可以访问任意的成员函数,包括const成员函数;
const对象的成员是不能修改的,而通过指针维护的对象确实可以修改的;
const成员函数不可以修改对象的数据,不管对象是否具有const性质。编译时以是否修改成员数据为依据进行检查。

常量与引用

 常量与引用的关系稍微简单一点。因为引用就是另一个变量的别名,它本身就是一个常量。也就是说不能再让一个引用成为另外一个变量的别名, 那么他们只剩下代表的内存区域是否可变。即:


 int i = 10;

    const int& ri = i;   // 正确:表示不能通过该引用去修改对应的内存的内容。


    int& const rci = i;  // 错误!不能这样写。

由此可见,如果我们不希望函数的调用者改变参数的值。最可靠的方法应该是使用引用。下面的操作会存在编译错误:

void func(const int& i)

    {

        // 错误!不能通过i去改变它所代表的内存区域。

        i = 100;

    }

    int main()

    {

        int i = 10;

        func(i);

        return 0;

    }

  这里已经明白了常量与指针以及常量与引用的关系。但是,有必要深入的说明以下。在系统加载程序的时候,系统会将内存分为4个区域:堆区 栈区全局区(静态)和代码区。从这里可以看出,对于常量来说,系统没有划定专门的区域来保护其中的数据不能被更改。也就是说,使用常量的方式对数据进行保护是通过编译器作语法限制来实现的。我们仍然可以绕过编译器的限制去修改被定义为“常量”的内存区域。看下面的代码:

 const int i = 10;

    // 这里i已经被定义为常量,但是我们仍然可以通过另外的方式去修改它的值。

    // 这说明把i定义为常量,实际上是防止通过i去修改所代表的内存。

    int *pi = (int*) &i;

常量函数

  常量函数是C++对常量的一个扩展,它很好的确保了C++中类的封装性。在C++中,为了防止类的数据成员被非法访问,将类的成员函数分成了两类,一类是常量成员函数(也被称为观察者);另一类是非常量成员函数(也被成为变异者)。在一个函数的签名后面加上关键字const后该函数就成了常量函数。对于常量函数,最关键的不同是编译器不允许其修改类的数据成员。例如:

 class Test

    {

    public:

        void func() const;

        private:

        int intValue;

    };

    void Test::func() const

    {

        intValue = 100;

    }

上面的代码中,常量函数func函数内试图去改变数据成员intValue的值,因此将在编译的时候引发异常。

    当然,对于非常量的成员函数,我们可以根据需要读取或修改数据成员的值。但是,这要依赖调用函数的对象是否是常量。通常,如果我们把一个类定义为常量,我们的本意是希望他的状态(数据成员)不会被改变。那么,如果一个常量的对象调用它的非常量函数会产生什么后果呢?看下面的代码:

    class Fred
    {

    public:

        void inspect() const;

        void mutate();

    };

    void UserCode(Fred& changeable, const Fred& unChangeable)

    {

        changeable.inspect(); // 正确,非常量对象可以调用常量函数。

        changeable.mutate(); // 正确,非常量对象也允许修改调用非常量成员函数修改数据成员。

        unChangeable.inspect(); // 正确,常量对象只能调用常理函数。因为不希望修改对象状态。

        unChangeable.mutate(); // 错误!常量对象的状态不能被修改,而非常量函数存在修改对象状态的可能

    }

 从上面的代码可以看出,由于常量对象的状态不允许被修改,因此,通过常量对象调用非常量函数时将会产生语法错误。实际上,我们知道每个成员函数都有一个隐含的指向对象本身的this指针。而常量函数则包含一个this的常量指针。如下:

    void inspect(const Fred* this) const;

    void mutate(Fred* this);

也就是说对于常量函数,我们不能通过this指针去修改对象对应的内存块。但是,在上面我们已经知道,这仅仅是编译器的限制,我们仍然可以绕过编译器的限制,去改变对象的状态。看下面的代码:

  class Fred
  {

  public:

        void inspect() const;
        private:
        int intValue;

   };

    void Fred::inspect() const

    {

        cout << "At the beginning. intValue = "<< intValue << endl;

        // 这里,我们根据this指针重新定义了一个指向同一块内存地址的指针。

        // 通过这个新定义的指针,我们仍然可以修改对象的状态。

        Fred* pFred = (Fred*)this;

        pFred->intValue = 50;

        cout << "Fred::inspect() called. intValue = "<< intValue << endl;

    }

    int main()

    {

        Fred fred;

        fred.inspect();
    
        return 0;

    }

   上面的代码说明,只要我们愿意,我们还是可以通过常量函数修改对象的状态。同理,对于常量对象,我们也可以构造另外一个指向同一块内存的指针去修改它的状态。这里就不作过多描述了。

    另外,也有这样的情况,虽然我们可以绕过编译器的错误去修改类的数据成员。但是C++也允许我们在数据成员的定义前面加上mutable,以允许该成员可以在常量函数中被修改。例如:

class Fred
{

public:

    void inspect() const;

private:

    mutable int intValue;

};

void Fred::inspect() const
{

    intValue = 100;

}

 但是,并不是所有的编译器都支持mutable关键字。这个时候我们上面的歪门邪道就有用了。关于常量函数,还有一个问题是重载。

#include <iostream>

#include <string>

    using namespace std;

    class Fred{

    public:

    void func() const;

    void func();

    };

    void Fred::func() const

    {

    cout << "const function is called."<< endl;

    }

    void Fred::func()

    {

    cout << "non-const function is called."<< endl;

    }

    void UserCode(Fred& fred, const Fred& cFred)

    {

    cout << "fred is non-const object, and the result of fred.func() is:" << endl;

    fred.func();

    cout << "cFred is const object, and the result of cFred.func() is:" << endl;

    cFred.func();

    }

    int main()

    {

    Fred fred;

    UserCode(fred, fred);

    return 0;

    }

 输出结果为:

    fred is non-const object, and the result of fred.func() is:

    non-const function is called.

    cFred is const object, and the result of cFred.func() is:

    const function is called.

 从上面的输出结果,我们可以看出。当存在同名同参数和返回值的常量函数和非常量函数时,具体调用哪个函数是根据调用对象是常量对像还是非常量对象来决定的。常量对象调用常量成员;非常量对象调用非常量的成员。

    总之,我们需要明白常量函数是为了最大程度的保证对象的安全。通过使用常量函数,我们可以只允许必要的操作去改变对象的状态,从而防止误操作对对象状态的破坏。但是,就像上面看见的一样,这样的保护其实是有限的。关键还是在于我们开发人员要严格的遵守使用规则。另外需要注意的是常量对象不允许调用非常量的函数。这样的规定虽然很武断,但如果我们都根据原则去编写或使用类的话这样的规定也就完全可以理解了。

 

 

 

C++中指针和引用的区别

1.指针和引用的定义和性质区别:

(1)指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:

int a=1;int *p=&a;

int a=1;int &b=a;

上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。

而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。

(2)可以有const指针,但是没有const引用;

(3)指针可以有多级,但是引用只能是一级(int **p;合法 而 int &&a是不合法的)

(4)指针的值可以为空,但是引用的值不能为NULL,并且引用在定义的时候必须初始化;

(5)指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了。

(6)"sizeof引用"得到的是所指向的变量(对象)的大小,而"sizeof指针"得到的是指针本身的大小;

(7)指针和引用的自增(++)运算意义不一样;

 

2.指针和引用作为函数参数进行传递时的区别。

(1)指针作为参数进行传递:

#include<iostream>
using namespace std;

void swap(int *a,int *b)
{
  int temp=*a;
  *a=*b;
  *b=temp;
}

int main(void)
{
  int a=1,b=2;
  swap(&a,&b);
  cout<<a<<" "<<b<<endl;
  system("pause");
  return 0;
}

结果为2 1;

用指针传递参数,可以实现对实参进行改变的目的,是因为传递过来的是实参的地址,因此使用*a实际上是取存储实参的内存单元里的数据,即是对实参进行改变,因此可以达到目的。

再看一个程序;

#include<iostream>
using namespace std;

void test(int *p)
{
  int a=1;
  p=&a;
  cout<<p<<" "<<*p<<endl;
}

int main(void)
{
    int *p=NULL;
    test(p);
    if(p==NULL)
    cout<<"指针p为NULL"<<endl;
    system("pause");
    return 0;
}

运行结果为:

0x22ff44 1

指针p为NULL

大家可能会感到奇怪,怎么回事,不是传递的是地址么,怎么p回事NULL?事实上,在main函数中声明了一个指针p,并赋值为NULL,当调用test函数时,事实上传递的也是地址,只不过传递的是指地址。也就是说将指针作为参数进行传递时,事实上也是值传递,只不过传递的是地址。当把指针作为参数进行传递时,也是将实参的一个拷贝传递给形参,即上面程序main函数中的p何test函数中使用的p不是同一个变量,存储2个变量p的单元也不相同(只是2个p指向同一个存储单元),那么在test函数中对p进行修改,并不会影响到main函数中的p的值。

(2)将引用作为函数的参数进行传递。

在讲引用作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。

看下面这个程序:

#include<iostream>
using namespace std;

void test(int &a)
{
  cout<<&a<<" "<<a<<endl;
}

int main(void)
{
    int a=1;
    cout<<&a<<" "<<a<<endl;
    test(a);
    system("pause");
    return 0;
}

输出结果为: 0x22ff44 1

          0x22ff44 1

再看下这个程序:

这足以说明用引用进行参数传递时,事实上传递的是实参本身,而不是拷贝。

所以在上述要达到同时修改指针的目的的话,就得使用引用了。

#include<iostream>
using namespace std;

void test(int *&p)
{
  int a=1;
  p=&a;
  cout<<p<<" "<<*p<<endl;
}

int main(void)
{
    int *p=NULL;
    test(p);
    if(p!=NULL)
    cout<<"指针p不为NULL"<<endl;
    system("pause");
    return 0;
}

输出结果为:0x22ff44 1

         指针p不为NULL

构造函数为什么不能是虚函数?

虚函数需要一个指向vtable虚函数表的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的。假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?所以构造函数不能是虚函数。

析构函数必须是虚函数?为什么C++默认的析构函数不是虚函数?

可以保证当我们new一个子类,然后使用基类指针指向该子类对象,释放基类指针时可以释放掉子类的空间,防止内存泄漏。 

C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。

虚函数和多态

多态的实现主要分为静态多态和动态多态

静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定。

虚函数的实现:

在有虚函数的类中,类的最开始部分是一个虚函数表的指针,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。

当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。

使用了虚函数,会增加访问内存开销,降低效率

虚函数和纯虚函数

虚函数:为了重载和多态的需要,在基类中是有定义的,即便定义是空,所以子类中可以重写也可以不写基类中的此函数。

纯虚函数:在基类中是没有定义的,必须在子类中加以实现。

例:

class Father

{

public:

  virtual void func_1(){}    //虚函数

  virtual void func_2()=0;  //纯函数

}



class Child

{

public:

  void func_1();    //可重写,也可不,重写格式可以和父类不同

  void func_2();    //必须重写,而且格式必须和父类保持一致

}

1. 虚函数和纯虚函数可以定义在同一个类(class)中,含有纯虚函数的类被称为抽象类(abstract class),而只含有虚函数的类(class)不能被称为抽象类(abstract class)。

  

2. 虚函数可以被直接使用,也可以被子类(sub class)重载以后以多态的形式调用,而纯虚函数必须在子类(sub class)中实现该函数才可以使用,因为纯虚函数在基类(base class)只有声明而没有定义。

 

3. 虚函数和纯虚函数都可以在子类(sub class)中被重载,以多态的形式被调用。

 

4. 虚函数和纯虚函数通常存在于抽象基类(abstract base class -ABC)之中,被继承的子类重载,目的是提供一个统一的接口。

 

5. 虚函数的定义形式:virtual {method body}

  纯虚函数的定义形式:virtual { } = 0;

在虚函数和纯虚函数的定义中不能有static标识符,原因很简单,被static修饰的函数在编译时候要求前期bind,然而虚函数却是动态绑定(run-time bind),而且被两者修饰的函数生命周期(life recycle)也不一样。

 

6. 虚函数必须实现,如果不实现,编译器将报错,错误提示为:

error LNK****: unresolved external symbol "public: virtual void __thiscall
ClassName::virtualFunctionName(void)"

 

7. 对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。

 

8. 实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖

该虚函数,由多态方式调用的时候动态绑定。

 

9. 虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的

函数

 

10. 多态性指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。
a.编译时多态性:通过重载函数实现
b 运行时多态性:通过虚函数实现。

 

11. 如果一个类中含有纯虚函数,那么任何试图对该类进行实例化的语句都将导致错误的产生,因为抽象基类(ABC)是不能被直接调用的。必须被子类继承重载以后,根据要求调用其子类的方法。

new和malloc的区别

一、new和delete

    C语言提供了malloc和free两个系统函数,完成对堆内存的申请和释放。而C++则提供了两个关键字new和delete;

1.1 规则

new/delete是关键字,效率高于malloc和free。
配对使用,避免内存泄漏和多重释放。
避免交叉使用,比如malloc申请空间delete释放,new出的空间被free。
new/delete 主要是用在类对象的申请和释放。申请的时候会调用构造器完成初始化,释放的时候,会调用析构器完成内存清理。

1.2 new/new[] 用法

    //开辟单地址空间
    int *p = new int;  //开辟大小为sizeof(int)空间
    int *q = new int(5); //开辟大小为sizeof(int)的空间,并初始化为5。
    //开辟数组空间
    //一维
    int *a = new int[100]{0};//开辟大小为100的整型数组空间,并初始化为0。
    //二维
    int (*a)[6] = new int[5][6];
    //三维
    int (*a)[5][6] = new int[3][5][6];
    //四维及以上以此类推。

1.3 delete/delete[] 用法

    //释放单个int空间
    int *a = new int;
    delete a;
    //释放int数组空间
    int *b = new int[5];
    delete []b;

二、new和malloc的区别

2.1 属性

    new和delete是C++关键字,需要编译器支持;malloc和free是库函数,需要头文件支持。

2.2 参数

    使用new操作符申请内存分配时无须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。

2.3 返回类型

    new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。

2.4 自定义类型


    new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。

    malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。

2.5 重载

    C++允许重载new/delete操作符,malloc不允许重载。

2.6 内存区域

    new做两件事:分配内存和调用类的构造函数,delete是:调用类的析构函数和释放内存。而malloc和free只是分配和释放内存。

    new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。

2.7 分配失败

    new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。

2.8 内存泄漏

    内存泄漏对于new和malloc都能检测出来,而new可以指明是哪个文件的哪一行,malloc确不可以。

重载和重写和重定义

重载(静态多态)

是函数名相同,参数列表不同 重载只是在类的内部存在。但是不能靠返回类型来判断

C++与C不同的地方在于,C编译的时候只会保留函数名,C++编译后会保留函数名加参数类型。

重写(动态多态)

重写override:也叫做覆盖。子类重新定义父类中有相同名称和参数的虚函数。函数特征相同。但是具体实现不同,主要是在继承关系中出现的 。

1、在子类中定义与父类中原型相同的函数

2、函数重写只发生在父类与子类之间

3、使用virtual关键字申明后能产生多态

4、运行期间根据具体对象类型决定调用的函数

重写需要注意:

  1. 被重写的函数不能是static的。必须是virtual的
  2. 重写函数必须有相同的类型,名称和参数列表
  3. 重写函数的访问修饰符可以不同。尽管virtual是private的,派生类中重写改写为public,protected也是可以的

重定义(隐藏)是指派生类的函数屏蔽了与其同名的基类函数

  1. 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
  2. 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。

Fork 函数

Fork:创建一个和当前进程映像一样的进程可以通过fork( )系统调用:

在子进程中,成功的fork( )调用会返回0。在父进程中fork( )返回子进程的pid。如果出现错误,fork( )返回一个负值

子进程复制父进程的数据空间(数据段)、栈和堆,父、子进程共享正文段

(也就是说,对于程序中的数据,子进程要复制一份,但是对于指令,子进程并不复制而是和父进程共享。)

C程序的存储空间布局

Fork,wait,exec函数

父进程产生子进程使用fork拷贝出来一个父进程的副本,此时只拷贝了父进程的页表,两个进程都读同一块内存,当有进程写的时候使用写实拷贝机制分配内存,

exec函数可以加载一个elf文件去替换父进程,从此父进程和子进程就可以运行不同的程序了。

fork从父进程返回子进程的pid,从子进程返回0.调用了wait的父进程将会发生阻塞,直到有子进程状态改变,执行成功返回0,错误返回-1。

exec执行成功则子进程从新的程序开始运行,无返回值,执行失败返回-1

C++中拷贝赋值函数的形参能否进行值传递

不能。如果是这种情况下,调用拷贝构造函数的时候,首先要将实参传递给形参,这个传递的时候又要调用拷贝构造函数。。如此循环,无法完成拷贝,栈也会满

C++智能指针

四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被c++11弃用。

为什么要使用智能指针:我们知道c++的内存管理是让很多人头疼的事,当我们写一个new语句时,一般就会立即把delete语句直接也写了,但是我们不能避免程序还未执行到delete时就跳转了或者在函数中没有执行到最后的delete语句就返回了,如果我们不在每一个可能跳转或者返回的语句前释放资源,就会造成内存泄露。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。下面我们逐个介绍。

auto_ptr

#include<iostream>
#include<memory>//auto_ptr的头文件
using namespace std;
class Test
{
public:
    Test(string s)
    {
        str = s;
       cout<<"Test creat\n";
    }
    ~Test()
    {
        cout<<"Test delete:"<<str<<endl;
    }
    string& getStr()
    {
        return str;
    }
    void setStr(string s)
    {
        str = s;
    }
    void print()
    {
        cout<<str<<endl;
    }
private:
    string str;
};
  
  
int main()
{
    auto_ptr<Test> ptest(new Test("123"));//调用构造函数输出Test creat
    ptest->setStr("hello ");//修改成员变量的值
    ptest->print();//输出hello
    ptest.get()->print();//输出hello
    ptest->getStr() += "world !";
    (*ptest).print();//输出hello world
    ptest.reset(new Test("123"));//成员函数reset()重新绑定指向的对象,而原来的对象则会被释放,所以这里会调用一次构造函数,还有调用一次析构函数释放掉之前的对象
    ptest->print();//输出123
    return 0;//此时还剩下一个对象,调用一次析构函数释放该对象
}

运行结果如下:

如上面的代码:智能指针可以像类的原始指针一样访问类的public成员,成员函数get()返回一个原始的指针,成员函数reset()重新绑定指向的对象,而原来的对象则会被释放。注意我们访问auto_ptr的成员函数时用的是“.”,访问指向对象的成员时用的是“->”。我们也可用声明一个空智能指针auto_ptr<Test>ptest();

当我们对智能指针进行赋值时,如ptest2 = ptest,ptest2会接管ptest原来的内存管理权,ptest会变为空指针,如果ptest2原来不为空,则它会释放原来的资源,基于这个原因,应该避免把auto_ptr放到容器中,因为算法对容器操作时,很难避免STL内部对容器实现了赋值传递操作,这样会使容器中很多元素被置为NULL

unique_ptr 

unique_ptr,是用于取代c++98的auto_ptr的产物,在c++98的时候还没有移动语义(move semantics)的支持,因此对于auto_ptr的控制权转移的实现没有核心元素的支持,但是还是实现了auto_ptr的移动语义,这样带来的一些问题是拷贝构造函数和复制操作重载函数不够完美,具体体现就是把auto_ptr作为函数参数,传进去的时候控制权转移,转移到函数参数,当函数返回的时候并没有一个控制权移交的过程,所以过了函数调用则原先的auto_ptr已经失效了.在c++11当中有了移动语义,使用move()把unique_ptr传入函数,这样你就知道原先的unique_ptr已经失效了.移动语义本身就说明了这样的问题,比较坑爹的是标准描述是说对于move之后使用原来的内容是未定义行为,并非抛出异常,所以还是要靠人肉遵守游戏规则.再一个,auto_ptr不支持传入deleter,所以只能支持单对象(delete object),而unique_ptr对数组类型有偏特化重载,并且还做了相应的优化,比如用[]访问相应元素等.

unique_ptr 是一个独享所有权的智能指针,它提供了严格意义上的所有权,包括:

1、拥有它指向的对象

2、无法进行复制构造,无法进行复制赋值操作。即无法使两个unique_ptr指向同一个对象。但是可以进行移动构造和移动赋值操作

3、保存指向某个对象的指针,当它本身被删除释放的时候,会使用给定的删除器释放它指向的对象

unique_ptr 可以实现如下功能:

1、为动态申请的内存提供异常安全

2、讲动态申请的内存所有权传递给某函数

3、从某个函数返回动态申请内存的所有权

4、在容器中保存指针

5、auto_ptr 应该具有的功能

unique_ptr 和 auto_ptr用法很相似,不过不能使用两个智能指针赋值操作,应该使用std::move; 而且它可以直接用if(ptest == NULL)来判断是否空指针;release、get、reset等用法也和auto_ptr一致,使用函数的返回值赋值时,可以直接使用=, 这里使用c++11 的移动语义特性。另外注意的是当把它当做参数传递给函数时(使用值传递,应用传递时不用这样),传实参时也要使用std::move,比如foo(std::move(ptest))。它还增加了一个成员函数swap用于交换两个智能指针的值

share_ptr

从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。出了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。、

weak_ptr

weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

c++11新特性

c++11的新特性

Struct和Class区别

面向过程的编程认为,数据和数据操作是分开的。然而当struct进入面向对象的c++时,其特性也有了新发展,就拿上面的错误函数来说,在c++中就能运行,因为在c++中认为数据和数据对象是一个整体,不应该分开,这就是struct在c和c++两个时代的差别。

在C++中struct得到了很大的扩充:

1.struct可以包括成员函数

2.struct可以实现继承

3.struct可以实现多态

strcut和class的区别

1.默认的继承访问权。class默认的是private,strcut默认的是public。

struct A
{
	int a;
};
 
struct B: A
{
	int b;
};

例如上边的代码,strcut B就是公有继承(public)的struct A。如果将strcut变为 class 那么将会是私有继承(private)这里就不做展示了所以我们在写类的时候都会显示的写出是公有继承还是私有继承 

当然,到底默认是public继承还是private继承,取决于子类而不是基类。struct可以继承class,同样class也可以继承struct,那么默认的继承访问权限是看子类到底是用的struct还是class。如下: 

struct A
{
	int a;
};
 
struct B: A   //共有继承
{
	int b;
};
 
class C: A    //私有继承
{
	int c
};

2.默认访问权限:struct作为数据结构的实现体,它默认的数据访问控制是public的,而class作为对象的实现体,它默认的成员变量访问控制是private的。

struct A
{
	int a;
};
int main()
{
	A n;
	n.a = 10;
	return 0;
}
 
//可以在类外访问成员变量,所以struct默认是共有的
 
class B
{
	int b;
};
 
int main()
{
 
	B n1;
	n1.b = 10;
	return 0;
}
 
//在内外无法访问私有变量

编译结果:

 3.“class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数

   从上面的区别,我们可以看出,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。

 4.class和struct在使用大括号{ }上的区别

关于使用大括号初始化
       1.)class和struct如果定义了构造函数的话,都不能用大括号进行初始化
  2.)如果没有定义构造函数,struct可以用大括号初始化。
  3.)如果没有定义构造函数,且所有成员变量全是public的话,class可以用大括号初始化。

C++类中的三大继承方式:公有继承、私有继承、保护继承

派生类继承了基类的全部数据成员和除了构造函数,析构函数之外的全部函数成员,但是,这些成员的访问属性在派生过程中是可以调整的。从基类继承的成员,其访问的英文属性由继承方式控制的。

所说这里的访问来自两个方面:一是派生类中的新增成员访问从基类继承的成员,的英文二在派生类的外部(非类族内的成员),通过派生类的对象访问从基类继承的成员。

类的继承方式有:公有继承,私有继承,保护继承

类的继承的方式为公有继承时:

基类的公共成员和受保护成员在派生类中访问属性(大众还是大众,受保护的还是保护的)还是不变,但是在类族之外的派生类的对象只能访问基类的公共成员,受保护和私有成员通过对象都不可以访问。在派生类中,只有基类的私有成员在派生类中不可直接访问。

类的继承的方式为私有继承时:

基类的公用成员和保护成员都以私人身份出现在派生类中,所以在类族之外的派生类的对象不能访问基类的公共和保护的成员,当然包括基类的私有成员。由于继承基类的函数可以说是派生类拥有对基类函数的调用权,所以在派生类中的其他成员可以直接访问基类的公共和保护成员,对于函数,一般采用在函数中调用基类的公共或保护函数的方式访问。

类的继承的方式为保护继承时:

基类的公用成员和保护成员都以保护身份出现在派生类中。在类族之外的派生类的对象和在派生类中的其他成员对于公众,保护,私人的访问权限与私有继承相同。

#include<iostream>  
using namespace std; 
 
class A 
{ 
private: 
    int privatetedateA;
protected:  
    int protecteddateA; 
public:
    int publicdateA;
};
  
class B :public A      
{  
public:  
    void funct()  
    {  
        int b;  
        b=privatedateA;   //error:基类中私有成员在派生类中是不可见的  
        b=protecteddateA; //true:基类的保护成员在派生类中为保护成员  
        b=publicdateA;    //true:基类的公共成员在派生类中为公共成员  
    }  
};  
/  
class C :private A  
{  
public:  
    void funct()  
    {  
        int c;  
        c=privatedateA;    //error:基类中私有成员在派生类中是不可见的  
        c=protecteddateA;  //true:基类的保护成员在派生类中为私有成员  
        c=publicdateA;     //true:基类的公共成员在派生类中为私有成员  
    }  
};  
 
class D :protected A  
{  
public:  
    void funct()  
    {  
        int d;  
        d=privatedateA;   //error:基类中私有成员在派生类中是不可见的  
        d=protecteddateA; //true:基类的保护成员在派生类中为保护成员  
        d=publicdateA;    //true:基类的公共成员在派生类中为保护成员  
    }  
};  
 
int main()  
{  
    int a;   
   
    B objB;  
    a=objB.privatedateA;   //error:基类中私有s成员在派生类中是不可见的,对对象不可见  
    a=objB.protecteddateA; //error:基类的保护成员在派生类中为保护成员,对对象不可见  
    a=objB.publicdateA;    //true:基类的公有成员在派生类中为公共成员,对对象可见  
   
    C objC;  
    a=objC.privatedateA;   //error:基类中私有成员在派生类中是不可见的,对对象不可见  
    a=objC.protecteddateA; //error:基类的保护成员在派生类中为私有成员,对对象不可见  
    a=objC.publicdateA;    //error:基类的公有成员在派生类中为私有成员,对对象不可见  
   
    D objD;  
    a=objD.privatedateA;   //error:基类中私有成员在派生类中是不可见的,对对象不可见  
    a=objD.protecteddateA; //error:基类的保护成员在派生类中为保护成员,对对象不可见  
    a=objD.publicdateA;    //error:基类的公有成员在派生类中为保护成员,对对象不可见  
   
    return 0;  
}  

父类子类构造析构顺序

c++中对象都是先构造成员变量,再调用自身的构造函数;故父类成员变量首先被构造,然后调用父类的构造函数,再构造子类的成员变量,最后调用子类的构造函数。

对象析构时与构造顺序相反,子类先调用自身的析构函数,再调用子类成员变量的析构函数,再调用父类的析构函数,再析构父类的成员变量。

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值