C++面试基础知识整理(2)

引用和指针的区别

  • 指针是一个实体,需要分配内存空间;引用只是变量的别名,不需要分配内存空间。
  • 引用在定义的时候必须进行初始化,并且不能够改变。指针在定义的时候不一定要初始化,并且指向的空间可变。
  • 指针和引用的自增运算结果不一样。(指针是指向下一个空间,引用时引用的变量值加1)
  • sizeof 引用得到的是所指向的变量(对象)的大小,而sizeof 指针得到的是指针本身的大小。
  • 引用访问一个变量是直接访问,而指针访问一个变量是间接访问。
  • 有多级指针,但是没有多级引用,只能有一级引用。

new/delete

问题

和malloc/free的区别

  • new/delete是C++关键字,需要编译器支持。malloc/free是库函数,需要头文件支持。
  • new会自动调用对象的构造函数,而malloc只是单纯的分配内存。
  • 参数:使用new操作符申请内存分配时不是必须指定内存块的大小,编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸。
  • 返回类型:new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,无须进行类型转换,故new是符合类型安全性的操作符。而malloc内存分配成功则是返回void * ,需要通过强制类型转换将void*指针转换成我们需要的类型。
  • 内存分配失败时:new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL。
  • 重载:new允许重载,malloc不允许
  • 内存区域:new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。

什么时候需要重载new/delete

  • 通过重载new和delete操作符,我们有机会在对象创建和释放的时候做一些内存管理的工作。比如,每次new一个Obj对象,我们递增new被调用的次数。delete的时候再递减。当程序退出时,我们检查该次数是否归0。如果不为0,则表示有Obj对象没有被delete,这很可能就是内存泄露的潜在原因。
  • 一般情况下,我们重载new和delete的目的是将内存创建和对象构造分隔开来。这样有什么好处呢?比如我们可以先创建一个大的内存,然后通过重载new函数将对象构造在这块内存中。当程序退出后,我们只要释放这个大内存即可。

const

作用

  1. 修饰变量,说明该变量不可以被改变;
  2. 修饰指针,分为指向常量的指针和指针常量;
  3. 常量引用,经常用于形参类型,即避免了拷贝,又避免了函数对值的修改;
  4. 修饰成员函数,说明该成员函数内不能修改成员变量。
  5. 修饰对象,常对象只能调用常成员函数、更新常成员变量

使用

// 类
class A
{
    
    
private:
    const int a;                // 常对象成员,只能在初始化列表赋值

public:
    // 构造函数
    A() : a(0) {
    
     };
    A(int x) : a(x) {
    
     };        // 初始化列表

    // const可用于对重载函数的区分
    int getValue();             // 普通成员函数
    int getValue() const;       // 常成员函数,不得修改类中的任何数据成员的值
};

void function()
{
    
    
    // 对象
    A b;                        // 普通对象,可以调用全部成员函数
    const A a;                  // 常对象,只能调用常成员函数、更新常成员变量
    a.getValue();				// 调用的是常成员函数
    
    const A *p = &a;            // 常指针
    const A &q = a;             // 常引用

    // 指针
    char greeting[] = "Hello";
    char* p1 = greeting;                // 指针变量,指向字符数组变量
    const char* p2 = greeting;          // 指针变量,指向字符数组常量(被const修饰的是char,说明char不可修改,是底层const)
    char const *p3 = greeting;			// 同上
    char* const p4 = greeting;          // 常指针,指向字符数组变量(被const修饰的是p4指针,说明指针不可修改,是顶层const)
    const char* const p5 = greeting;    // 常指针,指向字符数组常量(被const修饰的是char和p5指针,指针和指向对象的值都不能修改,第一个是底层const,第二个是顶层const)
}

// 函数
void function1(const int Var);           // 传递过来的参数在函数内不可变
void function2(const char* Var);         // 参数指针所指内容为常量
void function3(char* const Var);         // 参数指针为常指针
void function4(const int& Var);          // 引用参数在函数内为常量

// 函数返回值
const int function5();      // 返回一个常数
const int* function6();     // 返回一个指向常量的指针变量,使用:const int *p = function6();
int* const function7();     // 返回一个指向变量的常指针,使用:int* const p = function7();

问题

为什么常成员函数不能修改类中的数据成员?

class A
{
    
    
public:
    int m_ia;
	int setValue() const
    {
    
    
        m_ia=10;
    }
    // 常成员函数等价于
    int setValue(const A *this)
    {
    
    
        this->m_ia=10;// 修改常指针的值是错误的!
    }
}

static

作用

  1. 修饰普通变量,修改变量的存储区域和生命周期,使变量存储在静态区,在 main 函数运行前就分配了空间,如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
  2. 修饰普通函数,表明函数的作用范围,仅在定义该函数的文件内才能使用。在多人开发项目时,为了防止与他人命名空间里的函数重名,可以将函数定位为 static。
  3. 修饰成员变量,修饰成员变量使所有的对象只保存一个该变量,而且不需要生成对象就可以访问该成员。
  4. 修饰成员函数,修饰成员函数使得不需要生成对象就可以访问该函数,但是在 static 函数内不能访问非静态成员。

使用

  • 静态数据成员必须单独初始化
  • 静态成员函数不能调用非静态成员函数和非静态数据成员
  • 静态数据成员只有一份,且不依赖对象而存在。
class Tank
{
    
    
public:
    Tank(){
    
    s_iCount++;}
    ~Tank(){
    
    s_iCount--;}
    static int getCount(){
    
    return s_iCount;}
    static int s_iCount;
}
int Tank::s_iCount=0;// 单独初始化,不用再加static关键字
int main()
{
    
    
    cout<<Tank::getCount()<<endl;
    cout<<Tank::s_iCount<<endl;
    Tangk tank;
    cout<<tank.getCount()<<endl;
    cout<<tank.s_iCount<<endl;
    return 0;
}

问题

为什么不能在类的内部定义以及初始化static成员变量,而必须要放到类的外部定义

  • 答:因为静态成员属于整个类,而不属于某个对象,如果在类内初始化,会导致每个对象都包含该静态成员,这是矛盾的。静态的数据成员不依赖于对象的实例化。因此,也不会在类的构造函数中去初始化。

static关键字为什么只能出现在类内部的声明语句中,而不能重复出现在类外的定义中

  • 答:如果类外定义函数时在函数名前加了static,因为作用域的限制,就只能在当前cpp里用,类本来就是为了给程序里各种地方用的,其他地方使用类是包含类的头文件,而无法包含类的源文件。

强制类型转换符

const_cast

  • const_cast主要是用来移除变量的const限定符。
使用
int num_e = 4;
const int* p_e = &num_e;
int* p_f = const_cast<int*>(p_e);  	//使用时一定要知道num_e不是const的类型。
*p_f = 5;
cout << "num_e: " << num_e << endl;	// 输出5
cout << "p_e: " << *p_e << endl;	// 输出5
cout << "p_f: " << *p_f << endl;	// 输出5
  • 如果所指向的对象是个底层const
const int num_e = 4;				// 是个底层const
const int* p_e = &num_e;
int* p_f = const_cast<int*>(p_e); 
*p_f = 5;
cout << "num_e: " << num_e << endl;	// 输出4
cout << "p_e: " << *p_e << endl;	// 输出5
cout << "p_f: " << *p_f << endl;	// 输出5
  • 这种行为属于未定义行为,且num_e的值并未改变

  • 也可以使用const_cast将变量添加const限定符

const string& shorter(const string& s1, const string& s2) {
    
    
	return s1.size() <= s2.size() ? s1 : s2;
}

string& shorter(string& s1, string& s2) {
    
    
	auto &r = shorter(const_cast<const string&>(s1), const_cast<const string&>(s2));
	//auto等号右边为引用,类型会忽略掉引用
	return const_cast<string&>(r);
}

static_cast

  • 用于非多态类型的转换
  • 不执行运行时类型检查(转换安全性不如 dynamic_cast)
  • 通常用于转换数值数据类型(如 float -> int)
  • 可以在整个类层次结构中移动指针,子类转化为父类安全(向上转换),父类转化为子类不安全(因为子类可能有不在父类的字段或方法)

dynamic_cast

  • 运行时类型识别RTTI
  • 用于多态类型的转换
  • 执行行运行时类型检查
  • 只适用于指针或引用
  • 对不明确的指针的转换将失败(返回 nullptr),但不引发异常;引用转换失败后会抛出std::bad_cast异常。
  • 可以在整个类层次结构中移动指针,包括向上转换、向下转换

猜你喜欢

转载自blog.csdn.net/qq_34731182/article/details/113419312