c++c++面试知识6——第六章 语言特性相关

Posted 超级无敌陈大佬的跟班

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了c++c++面试知识6——第六章 语言特性相关相关的知识,希望对你有一定的参考价值。

目录

第六章 语言特性相关

6.1 ★左值和右值的区别?左值引用和右值引用的区别,如何将左值转换成右值?

6.2 什么是指针?指针的大小及用法?

6.3 什么是野指针和悬空指针?

6.4 C++ 11 nullptr 比 NULL 优势

6.5 ★指针和引用的区别?

6.6 ★常量指针和指针常量的区别

6.7 ★函数指针和指针函数的区别

6.8 ★强制类型转换有哪几种?

6.9 参数传递时,值传递、引用传递、指针传递的区别?

6.10 什么是模板?如何实现?

6.11 include " " 和 <> 的区别


第六章 语言特性相关

第六章重点介绍和 C++ 语言特性相关的知识点,涉及到指针、引用、强制类型转换等。在本章中也增加了设计模式相关知识点,重点对单例模式进行介绍,包括其实现思想,应用场景,代码实现等。

6.1 ★左值和右值的区别?左值引用和右值引用的区别,如何将左值转换成右值?

面试高频指数:★★★★☆

左值:指表达式结束后依然存在的持久对象。

右值:表达式结束就不再存在的临时对象。

左值和右值的区别:左值持久,右值短暂

右值引用和左值引用的区别:

  • 左值引用不能绑定到要转换的表达式、字面常量或返回右值的表达式。右值引用恰好相反,可以绑定到这类表达式,但不能绑定到一个左值上。
  • 右值引用必须绑定到右值的引用,通过 && 获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。

std::move ()函数:可以将一个左值强制转化为右值,继而可以通过右值引用使用该值,以用于移动语义。

#include <iostream>
using namespace std;

void fun1(int& tmp) { 
  cout << "fun1(int& tmp):" << tmp << endl; 
} 

void fun2(int&& tmp) { 
  cout << "fun2(int&& tmp)" << tmp << endl; 
} 

int main() { 
  int var = 11; 
  fun1(12); // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
  fun1(var);
  fun2(1); 
}

6.2 什么是指针?指针的大小及用法?

面试高频指数:★★☆☆☆

指针: 指向另外一种类型的复合类型。

指针的大小: 在 64 位计算机中,指针占 8 个字节空间。

#include<iostream>
using namespace std;

int main(){
    int *p = nullptr;
    cout << sizeof(p) << endl; // 8

    char *p1 = nullptr;
    cout << sizeof(p1) << endl; // 8
    return 0;
}

指针的用法:

  1. 指向普通对象的指针
  2. 指向常量对象的指针:常量指针
  3. 指向函数的指针:函数指针
  4. 指向对象成员的指针,包括指向对象成员函数的指针和指向对象成员变量的指针。
    特别注意:定义指向成员函数的指针时,要标明指针所属的类。
  5. this 指针:指向类的当前对象的指针常量。
#include <iostream>
using namespace std;

class A{
public:
    int var1, var2; 
    int add2(){
        return var1 + var2;
    }
    void set_name(string tmp)
    {
        //==this指针
        this->name = tmp;
    }
};

int add(int a, int b){
    return a + b;
}

int main()
{
    //==对象指针
    A *p = new A();
    
    //==常量指针
    const int c_var = 10;
    const int * p1 = &c_var;
    
    //==函数指针
    int (*fun_p)(int, int);
    fun_p = add;
    cout << fun_p(1, 6) << endl;

    //==指向成员变量的指针
    A ex;
    ex.var1 = 3;
    ex.var2 = 4;
    int *p = &ex.var1; // 指向对象成员变量的指针
    cout << *p << endl;
    
    //==指向成员函数的指针
    int (A::*fun_p)();
    fun_p = A::add; // 指向对象成员函数的指针 fun_p
    cout << (ex.*fun_p)() << endl;

    return 0;
}

6.3 什么是野指针和悬空指针?

面试高频指数:★★★☆☆

悬空指针:

  • 若指针指向一块内存空间,当这块内存空间被释放后,该指针依然指向这块内存空间,此时,称该指针为“悬空指针”。
void *p = malloc(size);
free(p); 
// 此时,p 指向的内存空间已释放, p 就是悬空指针。

野指针:

  • “野指针”是指不确定其指向的指针,未初始化的指针为“野指针”
void *p; 
// 此时 p 是“野指针”。

6.4 C++ 11 nullptr 比 NULL 优势

面试高频指数:★☆☆☆☆

NULL:预处理变量,是一个宏,它的值是 0,定义在头文件 <cstdlib> 中,即 #define NULL 0。
nullptr:C++ 11 中的关键字,是一种特殊类型的字面值,可以被转换成任意其他类型。

6.5 ★指针和引用的区别?

面试高频指数:★★★★☆

  • 指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变)
  • 指针本身在内存中占有内存空间,引用相当于变量的别名,在内存中不占内存空间。(是否占内存)
  • 指针可以为空,但是引用必须绑定对象。(是否可为空)
  • 指针可以有多级,但是引用只能一级。(是否能为多级)

6.6 ★常量指针和指针常量的区别

面试高频指数:★★★★☆

1、常量指针(常量在指针左侧):

  • 常量指针本质上是个指针,只不过这个指针指向的对象是常量。

特点:const 的位置在指针声明运算符 * 的左侧。只要 const 位于 * 的左侧,无论它在类型名的左边或右边,都表示指向常量的指针。(可以这样理解,* 左侧表示指针指向的对象,该对象为常量,那么该指针为常量指针。)

const int * p;
int const * p;

注意 1:不能通过这个指针去修改指针指向的常量对象,也就是说常量指针可以被赋值为变量的地址,之所以叫做常量指针,是限制了通过这个指针修改变量的值。

注意 2:虽然常量指针指向的对象不能变化,可是因为常量指针本身是一个变量,因此,指针可以被重新赋值,让指针指向其它的常量

#include <iostream>
using namespace std;

int main(){
    const int c_var = 8;
    const int c_var2 = 10;
    const int *p = &c_var; 
    ///===不能通过指针常量对象的值
    *p = 6;            // error: assignment of read-only location '* p'
    
    ///===可以修改指针的指向,让指针指向其它的常量对象
    p = &c_var2;
    return 0;
}

2、指针常量(常量在指针右侧):

  • 指针常量的本质上是个常量,只不过这个常量的值是一个指针。

特点:const 位于指针声明操作符右侧,表明该对象本身是一个常量,* 左侧表示该指针指向的类型;

  • 即以 * 为分界线,其左侧表示指针指向的类型,右侧表示指针本身的性质
const int var;
int * const c_p = &var; 

注意 1:指针常量的值是指针,这个值因为是常量,所以指针本身不能改变。

注意 2:指针的内容可以改变。

#include <iostream>
using namespace std;

int main(){
    int var, var1;

    //===指针常量
    int * const c_p = &var;

    //===不能修改指针的指向
    c_p = &var1; // error: assignment of read-only variable 'c_p'
    
    //===指针指向的变量的值可以修改
    *c_p = 12; 
    return 0;
}

6.7 ★函数指针和指针函数的区别

面试高频指数:★★★★☆

指针函数:

  • 指针函数本质是一个函数,只不过该函数的返回值是一个指针。相对于普通函数而言,只是返回值是指针。
#include <iostream>
using namespace std;

struct Type{
  int var1;
  int var2;
};

Type * fun(int tmp1, int tmp2){
    Type * t = new Type();
    t->var1 = tmp1;
    t->var2 = tmp2;
    return t;
}

int main(){
    Type *p = fun(5, 6);
    return 0;
}

函数指针:

  • 函数指针本质是一个指针变量,只不过这个指针指向一个函数。函数指针即指向函数的指针。
#include <iostream>
using namespace std;
int fun1(int tmp1, int tmp2){
  return tmp1 * tmp2;
}
int fun2(int tmp1, int tmp2){
  return tmp1 / tmp2;
}

int main(){
  int (*fun)(int x, int y); 
  fun = fun1;
  cout << fun(15, 5) << endl; 
  fun = fun2;
  cout << fun(15, 5) << endl; 
  return 0;
}
/*
运行结果:
75
3
*/

函数指针和指针函数的区别:

1)本质不同

  • 指针函数本质是一个函数,其返回值为指针。
  • 函数指针本质是一个指针变量,其指向一个函数。

2)定义形式不同

  • 指针函数:int* fun(int tmp1, int tmp2); ,这里* 表示函数的返回值类型是指针类型。
  • 函数指针:int (*fun)(int tmp1, int tmp2);,这里* 表示变量本身是指针类型。

3)用法不同

6.8 ★强制类型转换有哪几种?

面试高频指数:★★★★☆

1)static_cast:用于数据的强制类型转换,强制将一种数据类型转换为另一种数据类型。

  • 用于基本数据类型的转换。
  • 用于类层次之间的基类和派生类之间 指针或者引用 的转换(不要求必须包含虚函数,但必须是有相互联系的类),进行上行转换(派生类的指针或引用转换成基类表示)是安全的;进行下行转换(基类的指针或引用转换成派生类表示)由于没有动态类型检查,所以是不安全的,最好用 dynamic_cast 进行下行转换。
  • 可以将空指针转化成目标类型的空指针。
  • 可以将任何类型的表达式转化成 void 类型。

2)const_cast:强制去掉常量属性,不能用于去掉变量的常量性,只能用于去除指针或引用的常量性,将常量指针转化为非常量指针或者将常量引用转化为非常量引用(注意:表达式的类型和要转化的类型是相同的)。

3)reinterpret_cast:改变指针或引用的类型、将指针或引用转换为一个足够长度的整型、将整型转化为指针或引用类型。

4)dynamic_cast:

  • 其他三种都是编译时完成的,动态类型转换是在程序运行时处理的,运行时会进行类型检查。
  • 只能用于带有虚函数的基类或派生类的指针或者引用对象的转换,转换成功返回指向类型的指针或引用,转换失败返回 NULL;不能用于基本数据类型的转换。
  • 在向上进行转换时,即派生类类的指针转换成基类类的指针和 static_cast 效果是一样的,(注意:这里只是改变了指针的类型,指针指向的对象的类型并未发生改变)。
  • 在下行转换时,基类的指针类型转化为派生类类的指针类型,只有当要转换的指针指向的对象类型和转化以后的对象类型相同时,才会转化成功。

6.9 参数传递时,值传递、引用传递、指针传递的区别?

面试高频指数:★★☆☆☆

参数传递的三种方式:

  • 值传递:形参是实参的拷贝,函数对形参的所有操作不会影响实参。
  • 指针传递:本质上是值传递,只不过拷贝的是指针的值,拷贝之后,实参和形参是不同的指针,通过指针可以间接的访问指针所指向的对象,从而可以修改它所指对象的值。
  • 引用传递:当形参是引用类型时,我们说它对应的实参被引用传递。
#include <iostream>
using namespace std;

void fun1(int tmp){ // 值传递
    cout << &tmp << endl;
}

void fun2(int * tmp){ // 指针传递
    cout << tmp << endl;
}

void fun3(int &tmp){ // 引用传递
    cout << &tmp << endl;
}

int main(){
    int var = 5;
    cout << "var 在主函数中的地址:" << &var << endl;

    cout << "var 值传递时的地址:";
    fun1(var);

    cout << "var 指针传递时的地址:";
    fun2(&var);

    cout << "var 引用传递时的地址:";
    fun3(var);
    return 0;
}

/*
运行结果:
var 在主函数中的地址:0x23fe4c
var 值传递时的地址:0x23fe20
var 指针传递时的地址:0x23fe4c
var 引用传递时的地址:0x23fe4c
*/

说明:从上述代码的运行结果可以看出,只有在值传递时,形参和实参的地址不一样,在函数体内操作的不是变量本身。引用传递和指针传递,在函数体内操作的是变量本身

6.10 什么是模板?如何实现?

面试高频指数:★★★☆☆

模板:创建类或者函数的蓝图或者公式,分为函数模板和类模板。
实现方式:模板定义以关键字 template 开始,后跟一个模板参数列表。

  • 模板参数列表不能为空;
  • 模板类型参数前必须使用关键字 class 或者 typename,在模板参数列表中这两个关键字含义相同,可互换使用。

template <typename T>

template <typename T, typename U, ...>

函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。

  • 对于函数模板而言,模板类型参数可以用来指定返回类型或函数的参数类型,以及在函数体内用于变量声明或类型转换。
  • 函数模板实例化:当调用一个模板时,编译器用函数实参来推断模板实参,从而使用实参的类型来确定绑定到模板参数的类型。
#include<iostream>
using namespace std;

template <typename T>
T add_fun(const T & tmp1, const T & tmp2){
    return tmp1 + tmp2;
}

int main(){
    int var1, var2;
    cin >> var1 >> var2;
    cout << add_fun(var1, var2);

    double var3, var4;
    cin >> var3 >> var4;
    cout << add_fun(var3, var4);
    return 0;
}

6.11 include " " 和 <> 的区别

include<文件名> 和 #include"文件名" 的区别:

  • 查找文件的位置:
  1. include<文件名> 在标准库头文件所在的目录中查找,如果没有,再到当前源文件所在目录下查找;
  2. #include"文件名" 在当前源文件所在目录中进行查找,如果没有;再到系统目录中查找。
  • 使用习惯:
  1. 对于标准库中的头文件常用 include<文件名>,
  2. 对于自己定义的头文件,常用 #include"文件名"

____________________________________________________________

参考:

作者:力扣 (LeetCode)
链接:https://leetcode-cn.com/leetbook/read/cpp-interview-highlights/o5v4w6/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

 

以上是关于c++c++面试知识6——第六章 语言特性相关的主要内容,如果未能解决你的问题,请参考以下文章

第六章 网络编程-SOCKET开发

王爽《汇编语言》第三版 第六章 包含多个段的程序

第六章函数和宏定义

第六章

第六章 函数和宏定义

组原第六章练习题