【c++面试】c++语言基础

c++基础

4个强制类型转换

const_cast

const_cast 运算符仅用于进行去除 const 属性的转换,它也是四个强制类型转换运算符中唯一能够去除 const 属性的运算符。

1.使用场景:

a、常量指针转换为非常量指针,并且仍然指向原来的对象

b、常量引用被转换为非常量引用,并且仍然指向原来的对象

2.使用特点:

a、cosnt_cast是四种类型转换符中唯一可以对常量进行操作的转换符

b、去除常量性是一个危险的动作,尽量避免使用。

static_cast

static_cast 用于进行比较“自然”和低风险的转换。

1.使用场景:

a、用于类层次结构中基类和派生类之间指针或引用的转换;

上行转换(派生类---->基类)是安全的。

下行转换(基类---->派生类)由于没有动态类型检查,所以是不安全的。

b、用于基本数据类型之间的转换,如把int转换为char,安全性问题由开发者来保证;

c、把空指针转换成目标类型的空指针;

d、把任何类型的表达式转为void类型;

2.使用特点

a、主要执行非多态的转换操作,用于代替C中通常的转换操作;

b、隐式转换都建议使用static_cast进行标明和替换;

reinterpret_cast

reinterpret_cast 用于进行各种不同类型的指针之间、不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换。转换时,执行的是逐个比特复制的操作。

1.使用场景:

不到万不得已,不用使用这个转换符,高危操作;

2.使用特点:

a、reinterpret_cast是从底层对数据进行重新解释,依赖具体的平台,可移植性差;

b、reinterpret_cast可以将整型转换为指针,也可以把指针转换为数组;

c、reinterpret_cast可以在指针和引用里进行肆无忌惮的转换;

dynamic_cast

dynamic_cast专门用于将多态基类的指针或引用强制转换为派生类的指针或引用,而且能够检查转换的安全性。对于不安全的指针转换,转换结果返回 NULL 指针。

dynamic_cast 是通过“运行时类型检查”来保证安全性的。不能用于将非多态基类的指针或引用强制转换为派生类的指针或引用——这种转换没法保证安全性,只好用 reinterpret_cast 来完成。

1.使用场景:

只有在派生类之间转换时才使用dynamic_cast。

2.使用特点:

a、基类必须要有虚函数,因为dynamic_cast是运行时类型检查,需要运行时类型信息,而这个信息是存储在类的虚函数表中,只有一个类定义了虚函数,才会有虚函数表(如果一个类没有虚函数,那么一般意义上,这个类的设计者也不想它成为一个基类)。

b、对于下行转换,dynamic_cast是安全的(当类型不一致时,转换过来的是空指针),而static_cast是不安全的(当类型不一致时,转换过来的是错误意义的指针,可能造成踩内存,非法访问等各种问题)

c、dynamic_cast还可以进行交叉转换

在有继承关系的父子类中,构建和析构一个子类对象时,父子构造函数和析构函数的执行顺序分别是怎样的?

构造顺序:先基类,然后数据成员,最后自己。如果基类纵向有多个,则从上往下构造,如果横向有多个,按照继承表的顺序构造。
析构顺序和构造的顺序相反。

在有继承关系的类体系中,父类的构造函数和析构函数一定要申明为 virtual 吗?如果不申明为 virtual 会怎样?

是,一定要声明为虚,否则会有可能造成内存泄漏。如下代码,基类的析构不是虚函数,在main中用基类的指针指向了派生类的对象,delete的时候,调用了基类的析构函数,派生类没调用,派生类的对象没有释放内存,内存泄漏。

#include<iostream>
using namespace std;
//基类
class ClxBase{
public:
    ClxBase() {};
    //析构函数不是虚函数
    ~ClxBase() {
        cout << "Output from the destructor of class ClxBase!" << endl;
    };
 
    void DoSomething() {
        cout << "Do something in class ClxBase!" << endl;
    };
};
 
//派生类
class ClxDerived : public ClxBase{
public:
    ClxDerived() {};
    ~ClxDerived() {
        cout << "Output from the destructor of class ClxDerived!" << endl;
    };
 
    void DoSomething() {
        cout << "Do something in class ClxDerived!" << endl;
    }
};
  int main(){ 
      ClxBase *p =  new ClxDerived;
      p->DoSomething();
      delete p;
      return 0;
  } 
 
//运行结果
Do something in class ClxBase!
Output from the destructor of class ClxBase!

这段代码中基类的析构函数被定义为虚函数,在main函数中用基类的指针去操作派生类的成员。释放指针P的过程是:先释放了继承类的资源,再调用基类的析构函数。调用dosomething()函数执行的也是继承类定义的函数。

#include<iostream>
using namespace std;
 
//基类
class ClxBase{
public:
    ClxBase() {};
    virtual ~ClxBase() {
        cout << "Output from the destructor of class ClxBase!" << endl;
    };
    virtual void DoSomething() { 
        cout << "Do something in class ClxBase!" << endl;
    };
};
 
//派生类
class ClxDerived : public ClxBase{
public:
    ClxDerived() {};
    ~ClxDerived() { 
        cout << "Output from the destructor of class ClxDerived!" << endl;
    };
    void DoSomething() {
        cout << "Do something in class ClxDerived!" << endl;
    };
};
 
  int main(){ 
      //有多态
      ClxBase *p =  new ClxDerived;
      //当基类是虚函数时,基类的指针将表现为派生类的行为(非虚函数将表现为基类行为)
      p->DoSomething();
      delete p;
      return 0;
  }
 
//运行结果
Do something in class ClxDerived!
Output from the destructor of class ClxDerived!
Output from the destructor of class ClxBase!

所以,总结,如果一个基类指针指向了一个派生类对象,如果基类的析构不是虚函数,在delete基类指针的时候,只会调用基类的析构函数,不会调用派生类的析构,其实实现上并不是很理解?

什么是 C++ 多态?C++ 多态的实现原理是什么?

看懂陈皓这两篇就够了。

陈皓-c++对象内存布局上

陈皓-c++对象内存布局下

安全性 用虚函数表来干点什么坏事吧

水可载舟,亦可覆舟。

  1. 通过父类型的指针访问子类自己的虚函数。
    我们知道,子类没有重载父类的虚函数是一件毫无意义的事情。因为多态也是要基于函数重载的。虽然在上面的图中我们可以看到Base1的虚表中有Derive的虚函数,但我们根本不可能使用下面的语句来调用子类的自有虚函数:
          Base1 *b1 = new Derive();
            b1->f1();  //编译出错

任何妄图使用父类指针想调用子类中的未覆盖父类的成员函数的行为都会被编译器视为非法,所以,这样的程序根本无法编译通过。但在运行时,我们可以通过指针的方式访问虚函数表来达到违反C++语义的行为。(关于这方面的尝试,通过阅读后面附录的代码,相信你可以做到这一点)

  1. 访问non-public的虚函数。
    另外,如果父类的虚函数是private或是protected的,但这些非public的虚函数同样会存在于虚函数表中,所以,我们同样可以使用访问虚函数表的方式来访问这些non-public的虚函数,这是很容易做到的。
    如:
class Base {
    private:
            virtual void f() { cout << "Base::f" << endl; }
 
};
 
class Derive : public Base{
 
};
 
typedef void(*Fun)(void);
 
void main() {
    Derive d;
    Fun  pFun = (Fun)*((int*)*(int*)(&d)+0);
    pFun();
}

在C++ 程序中调用被C 编译器编译后的函数,为什么要加extern “C”?

答:首先,extern是C/C++语言中表明函数和全局变量作用范围的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。

通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。extern "C"是连接申明(linkage declaration),被extern "C"修饰的变量和函数是按照C语言方式编译和连接的。作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:void foo( int x, int y );该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字。这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。

所以,可以用一句话概括extern “C”这个声明的真实目的:解决名字匹配问题,实现C++与C的混合编程。

头文件中的ifndef/define/endif有什么作用?

答:这是C++预编译头文件保护符,保证即使文件被多次包含,头文件也只定义一次。

其实“被重复引用”是指一个头文件在同一个cpp文件中被include了多次,这种错误常常是由于include嵌套造成的。比如:存在a.h文件#include "c.h"而此时b.cpp文件导入了#include “a.h” 和#include "c.h"此时就会造成c.h重复引用。

头文件被重复引用引起的后果:
有些头文件重复引用只是增加了编译工作的工作量,不会引起太大的问题,仅仅是编译效率低一些,但是对于大工程而言编译效率低下那将是一件多么痛苦的事情。
有些头文件重复包含,会引起错误,比如在头文件中定义了全局变量(虽然这种方式不被推荐,但确实是C规范允许的)这种会引起重复定义。

是不是所有的头文件中都要加入#ifndef/#define/#endif 这些代码?
答案:不是一定要加,但是不管怎样,用#ifnde xxx #define xxx #endif或者其他方式避免头文件重复包含,只有好处没有坏处。个人觉得培养一个好的编程习惯是学习编程的一个重要分支。

#include<file.h> 与 #include "file.h"的区别?

答:前者是从标准库路径寻找和引用file.h,而后者是从当前工作路径搜寻并引用file.h。

const和#define有什么区别?

答:(1)const和#define都可以定义常量,但是const用途更广。

(2)const 常量有数据类型,而宏常量没有数据类型。编译器可以对前者进行类型安全检查。而对后者只进行字符替换,没有类型安全检查,并且在字符替换可能会产生意料不到的错误。

(3) 有些集成化的调试工具可以对const 常量进行调试,但是不能对宏常量进行调试。

关于sizeof小结

答:sizeof计算的是在栈中分配的内存大小。

(1) sizeof不计算static变量占得内存;

(2) 32位系统的指针的大小是4个字节,64位系统的指针是8字节,而不用管指针类型;

(3) char型占1个字节,int占4个字节,short int占2个字节

long int占4个字节,float占4字节,double占8字节,string占4字节

一个空类占1个字节,单一继承的空类占1个字节,虚继承涉及到虚指针所以占4个字节

(4) 数组的长度:

若指定了数组长度,则不看元素个数,总字节数=数组长度*sizeof(元素类型)

若没有指定长度,则按实际元素个数类确定

Ps:若是字符数组,则应考虑末尾的空字符。

(5) 结构体对象的长度

在默认情况下,为方便对结构体内元素的访问和管理,当结构体内元素长度小于处理器位数的时候,便以结构体内最长的数据元素的长度为对齐单位,即为其整数倍。若结构体内元素长度大于处理器位数则以处理器位数为单位对齐。

(6) unsigned影响的只是最高位的意义,数据长度不会改变,所以sizeof(unsigned int)=4

(7) 自定义类型的sizeof取值等于它的类型原型取sizeof

(8) 对函数使用sizeof,在编译阶段会被函数的返回值的类型代替

(9) sizeof后如果是类型名则必须加括号,如果是变量名可以不加括号,这是因为sizeof是运算符

(10) 当使用结构类型或者变量时,sizeof返回实际的大小。当使用静态数组时返回数组的全部大小,sizeof不能返回动态数组或者外部数组的尺寸

sizeof与strlen的区别?

答: (1)sizeof的返回值类型为size_t(unsigned int);

(2)sizeof是运算符,而strlen是函数;

(3)sizeof可以用类型做参数,其参数可以是任意类型的或者是变量、函数,而strlen只能用char*做参数,且必须是以’\0’结尾;

(4)数组作sizeof的参数时不会退化为指针,而传递给strlen是就退化为指针;

(5)sizeo是编译时的常量,而strlen要到运行时才会计算出来,且是字符串中字符的个数而不是内存大小;
sizeof 应用在C++中的类和结构的处理情况是相同的
但有两点需要注意:
1.结构或者类中的静态成员不对结构或者类的大小产生影响,因为静态变量的存储位置与结构或者类的实例地址无关。

2.没有成员变量的结构或类的大小为1,因为必须保证结构或类的每一个实例在内存中都有唯一的地址。

经过实践对于Linux如下代码分别输出的情况
C语言源文件 main.c 内容如下:

#include struct mystruct { }; 
int main(int argc, char *argv[]) 
{ 
    printf("%d\n",sizeof(struct mystruct)); 
    return 0; 
}
采用 gcc main.c 编译之后,输出结果显示 0 ,即空结构体大小为0。
采用 g++ main.c 编译之后,输出结果显示 1 ,即空结构体大小为1。
C++语言源文件 main.cpp 如下:
#include class myclass { };
struct mystruct { }; 
int main(int argc, char *argv[]) { 
    printf("%d,%d\n",sizeof(myclass),sizeof(struct mystruct)); 
    return 0; 
}
采用 gcc main.cpp 无法编译。

采用 g++ main.cpp 编译之后,输出结果显示 1,1 即空类和结构体大小均为1。

智能指针shared_ptr

#include <iostream>
#include <memory>
using namespace std;

int main() {
    {
        int a = 10;
        std::shared_ptr<int> ptra = std::make_shared<int>(a);
        std::shared_ptr<int> ptra2(ptra); //copy
        std::shared_ptr<int> ptra3 = ptra; 
        std::cout << ptra.use_count() << std::endl; // 3

        int b = 20;
        int *pb = &a;
        std::cout << ptra.use_count() << std::endl; // 3
        //std::shared_ptr<int> ptrb = pb;  //error
        std::shared_ptr<int> ptrb = std::make_shared<int>(b);
        ptra2 = ptrb; //assign
        pb = ptrb.get(); //获取原始指针

        std::cout << ptra.use_count() << std::endl; // 2
        std::cout << ptrb.use_count() << std::endl; // 2
        cout << *pb << endl; // 20
        ptra = ptrb;
        std::cout << ptra3.use_count() << std::endl; // 1
        std::cout << ptrb.use_count() << std::endl; // 3
        ptra3 = ptrb;
        cout << a << endl;
    }
}
运行结果:
3
3
2
2
20
1
3
10
  1. 智能指针的作用:
    1. 智能指针实质是一个对象,行为表现的却像一个指针;
    2. 解决忘记释放内存和一块内存释放多次;
    3. 智能指针还有一个作用是把值语义转换成引用语义。
  2. 使用
    1. shared_ptr多个指针指向相同的对象。shared_ptr使用引用计数,每一个shared_ptr的拷贝都指向相同的内存。每使用他一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。
    2. 智能指针在C++11版本之后提供,包含在头文件中,shared_ptr、unique_ptr、weak_ptr.
    3. unique_ptr的使用
        unique_ptr“唯一”拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。相比与原始指针unique_ptr用于其RAII的特性,使得在出现异常的情况下,动态资源能得到释放。unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。unique_ptr指针与其所指对象的关系:在智能指针生命周期内,可以改变智能指针所指对象,如创建智能指针时通过构造函数指定、通过reset方法重新指定、通过release方法释放所有权、通过移动语义转移所有权。
#include <iostream>
#include <memory>

int main() {
    {
        std::unique_ptr<int> uptr(new int(10));  //绑定动态对象
        //std::unique_ptr<int> uptr2 = uptr;  //不能賦值
        //std::unique_ptr<int> uptr2(uptr);  //不能拷貝
        std::unique_ptr<int> uptr2 = std::move(uptr); //轉換所有權
        uptr2.release(); //释放所有权
    }
    //超過uptr的作用域,內存釋放
}
  1. weak_ptr weak_ptr是为了配合shared_ptr而引入的一种智能指针,因为它不具有普通指针的行为,没有重载operator*和->,它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况。weak_ptr可以从一个shared_ptr或者另一个weak_ptr对象构造,获得资源的观测权。但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加

循环引用导致内存泄漏:

#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
private:
    std::shared_ptr<Child> ChildPtr;
public:
    void setChild(std::shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        if (this->ChildPtr.use_count()) {

        }
    }

    ~Parent() {
    }
};

class Child {
private:
    std::shared_ptr<Parent> ParentPtr;
public:
    void setPartent(std::shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    std::weak_ptr<Parent> wpp;
    std::weak_ptr<Child> wpc;
    {
        std::shared_ptr<Parent> p(new Parent);
        std::shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        std::cout << p.use_count() << std::endl; // 2
        std::cout << c.use_count() << std::endl; // 2
    }
    std::cout << wpp.use_count() << std::endl;  // 1
    std::cout << wpc.use_count() << std::endl;  // 1
    return 0;
}

正确做法:

#include <iostream>
#include <memory>

class Child;
class Parent;

class Parent {
private:
    //std::shared_ptr<Child> ChildPtr;
    std::weak_ptr<Child> ChildPtr;
public:
    void setChild(std::shared_ptr<Child> child) {
        this->ChildPtr = child;
    }

    void doSomething() {
        //new shared_ptr
        if (this->ChildPtr.lock()) {

        }
    }

    ~Parent() {
    }
};

class Child {
private:
    std::shared_ptr<Parent> ParentPtr;
public:
    void setPartent(std::shared_ptr<Parent> parent) {
        this->ParentPtr = parent;
    }
    void doSomething() {
        if (this->ParentPtr.use_count()) {

        }
    }
    ~Child() {
    }
};

int main() {
    std::weak_ptr<Parent> wpp;
    std::weak_ptr<Child> wpc;
    {
        std::shared_ptr<Parent> p(new Parent);
        std::shared_ptr<Child> c(new Child);
        p->setChild(c);
        c->setPartent(p);
        wpp = p;
        wpc = c;
        std::cout << p.use_count() << std::endl; // 2
        std::cout << c.use_count() << std::endl; // 1
    }
    std::cout << wpp.use_count() << std::endl;  // 0
    std::cout << wpc.use_count() << std::endl;  // 0
    return 0;
}

C++空类默认有哪些成员函数?

答:默认构造函数、析构函数、复制构造函数、赋值函数

多态类中的虚函数表是 Compile-Time,还是 Run-Time时建立的?

虚拟函数表是在编译期就建立了,各个虚拟函数这时被组织成了一个虚拟函数的入口地址的数组。

而对象的隐藏成员–虚拟函数表指针是在运行期–也就是构造函数被调用时进行初始化的,这是实现多态的关键。

一个父类写了一个 virtual 函数,如果子类覆盖它的函数不加 virtual ,也能实现多态?

在子类的空间里,有没有父类的这个函数,或者父类的私有变量? (华为笔试题)

答案:只要基类在定义成员函数时已经声明了 virtue关键字,在派生类实现的时候覆盖该函数时,virtue关键字可加可不加,不影响多态的实现。子类的空间里有父类的所有变量(static除外)。

完成字符串拷贝可以使用 sprintf、strcpy 及 memcpy 函数,请问这些函数有什么区别

答案:这些函数的区别在于 实现功能以及操作对象不同。

(1)strcpy 函数操作的对象是字符串,完成从源字符串到目的字符串的拷贝功能。

(2)sprintf 函数操作的对象不限于字符串:虽然目的对象是字符串,但是源对象可以是字符串、也可以是任意基本类型的数据。这个函数主要用来实现(字符串或基本数据类型)向字符串的转换功能。如果源对象是字符串,并且指定 %s 格式符,也可实现字符串拷贝功能。

(3)memcpy 函数顾名思义就是内存拷贝,实现将一个内存块的内容复制到另一个内存块这一功能。内存块由其首地址以及长度确定。程序中出现的实体对象,不论是什么类型,其最终表现就是在内存中占据一席之地(一个内存区间或块)。因此,memcpy

的操作对象不局限于某一类数据类型,或者说可适用于任意数据类型,只要能给出对象的起始地址和内存长度信息、并且对象具有可操作性即可。鉴于memcpy 函数等长拷贝的特点以及数据类型代表的物理意义,memcpy 函数通常限于同种类型数据或对象之间的拷贝,其中当然也包括字符串拷贝以及基本数据类型的拷贝。

对于字符串拷贝来说,用上述三个函数都可以实现,但是其实现的效率和使用的方便程度不同:

• strcpy 无疑是最合适的选择:效率高且调用方便。

• sprintf 要额外指定格式符并且进行格式转化,麻烦且效率不高。

• memcpy 虽然高效,但是需要额外提供拷贝的内存长度这一参数,易错且使用不便;并且如果长度指定过大的话(最优长度是源字符串长度 + 1),还会带来性能的下降。其实 strcpy 函数一般是在内部调用 memcpy 函数或者用汇编直接实现的,以达到高效的目的。因此,使用 memcpy 和 strcpy 拷贝字符串在性能上应该没有什么大的差别。

对于非字符串类型的数据的复制来说,strcpy 和 snprintf 一般就无能为力了,可是对 memcpy 却没有什么影响。但是,对于基本数据类型来说,尽管可以用 memcpy 进行拷贝,由于有赋值运算符可以方便且高效地进行同种或兼容类型的数据之间的拷贝,所以这种情况下 memcpy 几乎不被使用 。memcpy 的长处是用来实现(通常是内部实现居多)对结构或者数组的拷贝,其目的是或者高效,或者使用方便,甚或两者兼有。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值