C++面试常温问题(一)

还在整理ing


重点博客记得看 https://blog.csdn.net/gaojing303504/article/details/80338787
https://blog.csdn.net/zhang_guyuan/article/details/60590075
常用的技术基础主要包括:编程语言相关;数据结构,操作系统,计算机网络,计算机组成原理;数据库相关;linux命令行相关;以及常用的工具等等。

编程语言常用有关于C++,python,golang,JAVA等,主要会根据几种语言的特性来问。

1. C++和别的语言相比有什么特点?

C++语言和C语言相比

2. 什么是指针?

我自己对指针的理解,指针相当于一个索引,不是通过变量的地址直接寻址,而是把变量地址存到指针ipoint中,通过ipoint进行索引在找到i的地址。
总结指针就是“变量内存地址下存放的是另一个变量的地址”的变量。 因此&i这种也是一种指针。

int i = 5;
int* ipoint  = &i;   //指针ipoint为指向i的指针,即ipoint变量地址下存储的是i的地址
cout << *ipoint << endl;  //输出i的值
cout << ipoint << endl;   //输出i的地址
cout << &ipoint << endl;   //输出ipoint的地址
其中指针ipoint为指向i的指针,即ipoint变量地址下存储的是i的地址;
*ipoint 输出的是i的值;
ipoint 输出的是i的地址;
&ipoint 输出的是ipoint的地址。

下图1:整型变量i和指针变量ipoint的存储
整型变量i和指针变量ipoint的存储

2.1 符号 & 和 *

符号&就是取一个变量的地址,就是取一个内存地址下的变量内容。
但是
的操作对象只能是指针(指针包括&i类型的指针)

int i = 5;
int* ipoint = *i;   //==语法错误==,虽然int*和int*类型匹配,但是*的操作对象只能是指针;
cout << *(&i) <<endl;   //输出i的值,先取i的地址再根据i的地址取值。
int* ipoint = *i;   ===语法错误===,虽然int*和int*类型匹配,但是*的操作对象
只能是指针;
*(&i) 没有语法错误,先取i的地址再根据i的地址取值,输出的是i的值。

2.2 是复制还是指向问题

int* temp;
是声明一个指针变量temp,相当于只是对指针变量temp分配了空间,但是temp是一个空指针,因为没有对指向内容*temp分配空间。

  • 复制问题:要求temp和*temp都要有内存空间。
    以下代码是复制问题,直接更改指针指向内存下的内容,。
    但是因为没有对 *temp分配空间,所以会编译报错。但是dev上面可以运行,因为dev会在复制时临时给一个随机地址存值,但是这种随机的空间没办法回收,会造成内存泄漏
void swap2(int* a,int* b)
{
	int* temp;
	*temp = *a;
}
  • 指向问题:只要求temp本身有空间,对*temp没有要求。
    以下代码是指向问题,直接改的是指针指向的地址。
void swap3(int* a,int* b)
{
	int* temp;
	temp = a;
	a = b;
	b = temp;
}

2.3 字符指针char*

字符指针是一个稍微特殊一点点的指针,平常看起来会直接拿来当成字符串用,但是本质上还是指针。

  1. 赋值:可以直接用字符串的方式进行赋值,直接赋一个字符数组;
char* str = "hello";
  1. 输出:
  • 从字符指针的本质看,str地址下存储的是字符数组的首地址即‘h’的地址,但是直接cout输出看,会直接打印出整个字符数组char[] 的内容:
char* str = "hello";
cout << str << endl;   //输出结果:hello
cout << str[0] << endl;   //输出结果:h
cout << *str << endl;   //输出结果:h
cout << &str << endl;   //输出结果:0031F99C
str 输出结果:hello;
str[0] 输出结果:h;
*str 输出结果:h;
&str 输出结果:0031F99C。

下图2:字符指针变量str和指向字符h以及连续字符的存储
图2:字符指针变量str和指向字符h以及连续字符的存储

  • 但是为什么cout << str 会直接输出整个字符数组char[]的内容,而不只是指向的数组首地址?
    原因: << 对字符串的重载,对str对整个字符数组char[]输出。详情见下:C++指针困惑,为什么char *p cout 直接输出了整个字符数组而不是输出首个地址
  • 现在已知cout << str输出不是指向字符‘h’的地址,如果非要输出指针指向字符变量的地址:使用强制类型转换的方法把str转换成一般指针,输出时就不会再因为 << 对字符指针的重载输出hello了。
char* str = "hello";
cout << &str << endl;    //输出为str变量的地址,即图2中的地址1
cout << (void*) str << endl;   //类型强制转化,输出为指针指向变量的地址,即地址2
&str 输出为str变量的地址,即图2中的地址1;
(void*) str 是类型强制转化,输出为指针指向变量的地址,即地址2。
  1. 由char* str = "hello"赋值导致的**程序崩溃**问题:
char* str = "hello";
strcpy(str, "olleh");
cout << str << endl;
  • 以上代码平平无奇,但是却会导致程序崩溃。原因是因为由 = 对char* 变量赋值时,系统在常量区给“hello”开了空间,指针str指向字符‘hello’,即str指向的是常量。即当前指向地址下的内容不可以更改,但是可以直接更改指针指向的地址本身。
  • 修改指向地址下的内容:导致崩溃
char* str = "abcd";   // ==程序崩溃==
str[0] = 'p';
char* str = "abcd";  会导致 ===程序崩溃===
  • 修改指针指向的地址本身:指向一个新的对象变量
char* str = "abcd";
str = "dbca";   //相当于直接改了指针变量下存储的变量地址,将“dbca”换成十六进制
cout << str << endl;
char* str = "abcd" 先给str分配指向对象,
str = "dbca" 更改指针变量下存储的变量地址,将“dbca”换成十六进制相当于存的地址。
  • 需要修改指向对象的值:不用char*而用char[]
char str[6] = "hello";
strcpy(str, "olleh");
cout << str << endl;   //一段正常的程序,输出为olleh

2.4 指向数组的指针

  1. 指向一维数组:两种方式
  • 直接a 进行赋值,a本身就代表数组a的首地址;
int a[10];
int* apoint;
apoint = a;
  • 表达的更清楚一点,取首元素a[0]的地址赋值;
int a[10];
int* apoint;
apoint = &(a[0]);
  1. 指向二维数组:*apoint才表示指向也是指向二维数组的指针,apoint表示指向(*apoint)的指针。
  • 定义方法1:不用先声明二维数组,直接声明指针
int (*apoint)[3][6];
cout << sizeof(*apoint) << endl;
  • 定义方法2:先声明一个二维数组,然后声明指向该数组的指针
int a[3][6];
int** apoint = NULL;
*apoint = &(a[0][0]);   //*apoint已经表示指向二维数组的指针,存储的是二维数组首元素的地址;
(*apoint)表示apoint是指向二维数组int [3][6]数组的指针,并不是取内容,大小为72字节;
(**apoint)表示指向一维数组int [6]的指针,大小为sizeof(int) * 6 = 32字节;
(***apoint)表示指向int的指针,大小为sizeof(int) = 4字节;
  • 指针初始化1:和一维数组 apoint = a 初始化不一样,取apoint = &a
int a[3][6];
int (*apoint)[3][6];
(apoint) = &a;
  • 指针初始化2:取*apoint = &(a[0][0])
int a[3][6];
int (*apoint)[3][6];
(*apoint) = &a[0][0];
  • 指针初始化3:取(*apoint) = a这样会有编译错误
    在这里插入图片描述
  • 一个复杂的例子:
double* (*a)[3][6];
cout << sizeof(a) << endl;   //a只是指针,指向指针(double*) [3][6],即a内存地址下存的是地址,大小为4字节;
cout << sizeof(*a) << endl;   //*a就是(double*)[3][6],大小为sizeof(double*) * 3 * 6 = 72字节;
cout << sizeof(**a) << endl;   //**a就是(double*)[6],大小为sizeof(double*) * 6 = 24字节;
cout << sizeof(***a) << endl;   //***a就是(double*),大小为sizeof(double*) = 4字节;
cout << sizeof(****a) << endl;   //****a就是double,大小为sizeof(double) = 8字节;
sizeof(a) 大小为4字节,因为a是指针,内存下存的就是一个地址;
sizeof(*a) = sizeof(double*) * 3 * 6 = 72字节,因为*a内存下就是double(*)[3][6];
sizeof(**a)  = sizeof(double*) * 6 = 24字节,因为**a内存下就是(double*)[6];
sizeof(***a) = sizeof(double*) = 4字节,因为***a内存下就是(double*);
sizeof(****a) = sizeof(double) = 8字节,因为****a内存下就是一个double;

3. 指针*和引用&的区别?

3.1 NULL问题:指针可以为空指针,但是引用不可以为空引用

  1. 从定义上讲:引用相当于一个变量的别名,定义时一定要有初始化,并不能声明一个不指向任何对象的引用。
  2. 指针可以为空,它是值指向某个变量地址的变量,空指针即不指向任何对象。

3.2 改变指向对象问题:指针可以改变指向的对象,引用不可以

  1. “至死不渝”的引用: 不可以改变指向的对象,不能从变量a的别名改成变量b的别名;
int i = 13;
int j = 1313;
int& iref = i;
iref = j;
cout << i << " " << iref << endl;   //输出结果为:1313 1313
以上代码编译没有问题,但是并没有改变iref的指向,iref = j相当于直接使用引
用的存储地址进行赋值,由于引用是共享地址,所以相当于直接对i进行了赋值。
  1. “花心大萝卜”的指针:可以改变指向的对象,只要改变内存下存储的地址就指向新的对象,和之前指向的对象不再有关联。
int i = 13;
int j = 1313;
int* ipoint = &i;
ipoint = &j;
cout << i << " " << *ipoint << endl;   //输出结果为:13 1313
以上代码ipoint本来指向对象变量i,后来改了指向对象变量b。

3.3 内存问题:指针变量有自己的空间,而引用和指向变量共享空间

  1. 引用只是别名,和指向变量本体共享空间;
  2. 指针有自己的空间,和指向的对象类型一致,但是本质上没有直接的关联;

3.4 使用时引用比指针更安全

  1. 指针可以随意切换指向对象;
  2. 指针可以不被初始化;
  3. 指针使用时要检验是否为NULL;
  4. const指针虽然不能改变指向,但是仍然可能有NULL问题;
  5. 可能有野指针的问题:空指针是指指针目前为空闲,没有指向任何对象;而野指针是指一个指针指向了一块不可使用的内存空间,产生原因主要有三个:
  • 指针没有进行初始化:任何指针变量初始化时不会自动设置为NULL指针,它的设置是随机的,可能指向一块不可使用的内存空间,变成野指针;
  • delete或者free的时候,只是释放了指针指向的内存空间释放掉,如果没有将指针置为NULL,该指针变成野指针。另外如果有多个指针指向同一块内存区域,当一个指针delete或者free,其他指针都将变成野指针;
  • 当指针操作超出了指向内存空间的作用范围,此时指针越界也会变成一个野指针;

4. 类的问题

4.1 public, protected, private

  1. 属性:
    private: 只能由该类中的函数访问、其友元函数访问,不能被任何其他访问,该类的对象也不能访问;
    protected: 可以被该类中的函数、子类的函数、以及其友元函数访问,但不能被该类的对象访问;
    public: 可以被该类中的函数、子类的函数、其友元函数访问,也可以由该类的对象访问;
    注:友元函数包括两种:设为友元的全局函数,设为友元类中的成员函数
  2. 继承方式:
    public继承:不改变基类成员的访问权限;
    private继承:使得基类所有成员在子类中的访问权限变为private
    protected继承:将基类中public成员变为子类的protected成员,其它成员的访问 权限不变。

4.2 浅拷贝和深拷贝

简单说【浅拷贝】是增加了一个指针,指向原来已经存在的内存。而【深拷贝】新开辟了一块空间。

  1. 浅拷贝:
    只是增加了一个指针,指向原来存在对象的内存,共享内存。缺点为:
    当一个对象值有改变,另一个对象的值随之改变;
    当其中一个对象释放了内存,另一个对象指针将变成野指针;
    当类中的两个对象指向同一个内存空间,一个对象的析构函数释放空间后,另一个对象再次执行析构函数会出现错误;
  2. 深拷贝:
    开辟了一块新的内存地址用于存放复制的对象,只拷贝内容。

4.3 类中浅拷贝和深拷贝的实现—拷贝构造函数

当没有自定义拷贝构造函数时,编译器会自动写一个拷贝构造函数,实现方式为浅拷贝

class String
{
private:
    char* pstr;
public:
#if !is_deep_copy   
//浅拷贝方式实现的拷贝构造函数,不会为新对象中的属性分配空间,
//只是把对象内的属性指向同一块内存。
    String(const String& s):pstr(s.pstr)
    {}
#endif
    
#if is_deep_copy
//深拷贝方式实现的拷贝构造函数,先重新分配空间,再复制内容。
    String(String& s):pstr(new char[strlen(s.pstr)+1])
    {
        strcpy(pstr,s.pstr);
    }
#endif
}

4.4 拷贝构造函数和=运算符重载问题

class String
{
private:
    char* pstr;
    
public:
    String(const char *pStr = " ")
    {
        if(pStr == NULL)
        {
            pstr = new char[1];
            *pstr = '\0';
        }
        else{
            pstr = new char[strlen(pStr) + 1];
            strcpy(pstr, pStr);
        }
    }
    
#if !is_deep_copy
    String(const String& s):pstr(s.pstr)
    {}    
    String& operator=(const String& s)
    {
        if(this != &s)
        {
            delete[] pstr;
            strcpy(pstr,s.pstr);
        }
        return *this;
    }
#endif
    
#if is_deep_copy
    String(String& s):pstr(new char[strlen(s.pstr)+1])
    {
        strcpy(pstr,s.pstr);
    }
    String& operator=(const String& s)
    {
        if(this != &s)
        {
            char* temp = new char[strlen(s.pstr) + 1];
            delete[] pstr;
            strcpy(temp,s.pstr);
            pstr = temp;
        }
        return *this;
    }
#endif   
    ~String()
    {
        if(pstr != NULL)
        {
            delete[] pstr;
            pstr = NULL;
        }
    }
};
int main()
{
    String s1("hello");   //调用构造函数
    String s2 = s1;        //调用拷贝构造函数
    String s3("nice to meet you");   //调用构造函数
    s3 = s2;     //调用=运算符重载函数
    return 0;
}
其中String s1("hello");   调用构造函数
String s2 = s1;           看起来是调用了运算符=,但其实是调用拷贝构造函数。
s3 = s2;                  调用=运算符重载函数

对象进行赋值时,调用=运算符重载函数或者是拷贝构造函数?
主要看 是否创造了新对象,产生新对象即为拷贝构造函数,未产生新对象即为=运算符重载函数
对于s2是在用s1赋值时产生的新对象,因此调用的是拷贝构造函数;
对于s3是已经创建好的对象,再利用s2对s3进行赋值时并未产生新对象,因此此处调用的=运算符重载函数。

5. sizeof()的问题?

5.1 sizeof是关键字而不是函数

首先得知道sizeof是c语言中的一个关键字而不是函数,在编译阶段就已经确定。

int a = 1;
int b = sizeof(++a);
cout << a << endl;
由于sizeof不是一个函数,关键字在编译阶段就已经确定,所以括号中不会被执行,
因此输出为结果:1。

5.2 一般变量的sizeof大小

在这里插入图片描述

变量类型sizeof()
char1字节
short2字节
int4字节
unsigned int4字节
unsigned = unsigned int4字节
float4字节
double8字节
__int648字节

5.3 class和struct的sizeof

- 空class和struct:编译器仍留1字节的空间

struct A{};
class B{};
int main()
{
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
}
空的结构体A和类B,sizeof(A)和sizeof(B)都为1字节。

- 非空class和struct:编译器不会多给1字节

struct A{ int a; };
class B{};
int main()
{
	cout << sizeof(A) << endl;
	cout << sizeof(B) << endl;
}
对于空的结构体和类,仍然会分配一个1字节的空间,但是对于非空的类和结构体,
不会单独多加1字节的空间,因此sizeof(A)和sizeof(B)分别为4字节和1字节;

- class和struct整体空间一定是占用空间最大的元素的整数倍,否则要有数据对齐

对于class和struct,对于一般的变量元素,直接以元素的sizeof进行补齐;
但是对于数组类型的元素,不是看成一个整体,而是以元素为单位进行补齐。

class s1
{
    char a[8];
};
struct s2
{
    double d;
};
class s3
{
    s1 s;
    char ch;
};
struct s4
{
    s2 s;
    char ch;
};

int main()
 {
    cout <<sizeof(s1) << endl;
    cout <<sizeof(s2) << endl;
    cout <<sizeof(s3) << endl;
    cout <<sizeof(s4) << endl;
}
sizeof(s1)输出结果为8字节,sizeof(s2)输出结果为8字节;
sizeof(s3)输出结果为9字节,sizeof(s4)输出结果为16字节;
对于s4来说,s2大小为8字节,需要进行对齐,因此两个元素对齐8字节为16字节;
但是对于s3来说,s1虽然为8字节,但是对于数组是每个元素单独存放,以元素大小
为单位进行对齐,因此s3中相当于9个char元素,不需要特别补齐。

- class中static成员变量不算入对象内存空间,const算入

class B
{
	int a;
	const int b;
	static int c;
};
int main()
{
	cout << sizeof(B) << endl;
}
sizeof(B)大小为8字节,其中类中的static成员变量是属于类域而不是属于对象的,
因此static的成员变量不算入class B的内存大小;但是const成员变量算入class B的
内存大小;

- class一般成员函数不算入对象内存空间,如果有virtual成员函数,对象中会包含一个指向虚函数表的指针

class B
{
	int fun(int b){ return b; };
	virtual void func(){ int x = 0; };
};
int main()
{
	cout << sizeof(B) << endl;
}
sizeof(B)大小为4字节,其中一般的函数fun()没有计入对象的空间大小中,但是对于
virtual函数func(),对象包含一个指针指向虚函数表,因此指针大小为4字节。

5.4 与strlen()函数的比较

- strlen()计算数组的长度,长度不包含结束符

  1. strlen()是用来计算数组长度的函数,用’\0’ 作为数组结束符作为判断;
  2. 统计单位为个,即 统计数组中有多少个元素;
  3. 最后统计的长度 不会包含’\0’结束符;

- sizeof()统计数据所用内存空间,包含结束符占用空间

  1. sizeof()是用来统计数据所占内存空间的大小;
  2. 用字节数表示占用内存空间;
  3. 最后统计的空间 会包含结束符’\0’在内;

5.5 指针和静态数组的sizeof()问题

- 指针内存大小是指向对象地址大小,32位系统就是4字节,64位就是8字节

对于指针来说,不管指向的对象是一般变量或者类或者结构体,不管指向对象的内存大小,都是指对象的地址。因此 32位系统的指针大小全部为4字节,64系统的指针大小全部为8字节。

- 静态数组作为形参使用时,数组名称当做指针使用

void f(int p[])
{
	cout << sizeof(p) << endl;
}
int main()
{
	int p[5];
	f(p);
}
当系统为32位时f(p)输出的结果为4字节,64系统时输出的结果为8字节,因为数组p
作为函数形参使用时,被当做一个指针指向原数组,因此输出p的大小为指针大小。

- 对于字符数组,sizeof()多计算末尾结束符’\0’的大小

char ch[] = "hello";
cout << sizeof(ch) << endl;
sizeof(ch)输出结果为6字节,因为hello5个字符加一个'\0'字节,用6字节。

- 指向二维数组的指针的复杂问题

double* (*a)[3][6];
cout << sizeof(a) << endl;   //a只是指针,指向指针(double*) [3][6],即a内存地址下存的是地址,大小为4字节;
cout << sizeof(*a) << endl;   //*a就是(double*)[3][6],大小为sizeof(double*) * 3 * 6 = 72字节;
cout << sizeof(**a) << endl;   //**a就是(double*)[6],大小为sizeof(double*) * 6 = 24字节;
cout << sizeof(***a) << endl;   //***a就是(double*),大小为sizeof(double*) = 4字节;
cout << sizeof(****a) << endl;   //****a就是double,大小为sizeof(double) = 8字节;
sizeof(a) 大小为4字节,因为a是指针,内存下存的就是一个地址;
sizeof(*a) = sizeof(double*) * 3 * 6 = 72字节,因为*a内存下就是double(*)[3][6];
sizeof(**a)  = sizeof(double*) * 6 = 24字节,因为**a内存下就是(double*)[6];
sizeof(***a) = sizeof(double*) = 4字节,因为***a内存下就是(double*);
sizeof(****a) = sizeof(double) = 8字节,因为****a内存下就是一个double;
  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值