C/C++笔试易错与高频题型&图解知识点(二)—— C++部分

目录

1.构造函数初始化列表

1.1 构造函数初始化列表与函数体内初始化区别

1.2 必须在初始化列表初始化的成员

2 引用&引用与指针的区别

2.1 引用初始化以后不能被改变,指针可以改变所指的对象

 2.2 引用和指针的区别

3 构造函数与析构函数系列题

3.1构造函数与析构函数的调用次数

4 类的运算符重载

5 类的静态数据成员

5.1 malloc/new/new[]

5.2 new的实现步骤与细节

6 this指针相关题目 

6.1 this可以为空吗?

6.2 this指针存放在哪里?

6.3 delete this

7 其他于类相关的题目

7.1 空类的大小

7.2 对const变量的修改

  volatile

 7.3 赋值运算符重载



1.构造函数初始化列表

有一个类A,其数据成员如下: 则构造函数中,成员变量一定要通过初始化列表来初始化的是:______。

class A {
...
private:
    int a;
public:
    const int b;
    float* &c;
    static const char* d;
    static double* e;
};

A. a b c

B. b c

C. b c d e

D. b c d

E. b

F. c

答案:B

知识点:

1.1 构造函数初始化列表与函数体内初始化区别

一个类,其包含一个类类型成员,对于它的构造函数,如果在函数体内初始化,会先调用其类类型成员的默认构造函数,再调用赋值运算符;而在构造函数初始化时会直接调用它的拷贝构造函数进行初始化

函数体类初始化:

#include <iostream>

class B {
public:
	B() { std::cout << "B defualt construct" << '\n'; }
	B(int t) : _t(t) { std::cout << "B construct" << '\n'; }
	B(const B& b) : _t(b._t) { std::cout << "B copy construct" << '\n'; }
	B& operator=(const B& b) {
		_t = b._t;
		std::cout << "B assign operator"<< '\n';
		return *this;
	}
private:
	int _t = 0;
};
class A {
public:
	A() { std::cout << "A defualt construct" << '\n'; }
	A(const B& b){ 
		puts("---------------------");
		_b = b;
		std::cout << "A construct" << '\n'; 
	}

	A(const A& a) : _b(a._b) { std::cout << "A copy construct" << '\n'; }
	A& operator=(const A& a) {
		_b = a._b;
		std::cout << "A assign operator" << '\n';
		return *this;
	}
private:
	B _b;
};
int main() {
	B b(1);
	A a(b);
}

初始化列表初始化:

#include <iostream>

class B {
public:
	B() { std::cout << "B defualt construct" << '\n'; }
	B(int t) : _t(t) { std::cout << "B construct" << '\n'; }
	B(const B& b) : _t(b._t) { std::cout << "B copy construct" << '\n'; }
	B& operator=(const B& b) {
		_t = b._t;
		std::cout << "B assign operator"<< '\n';
		return *this;
	}
private:
	int _t = 0;
};
class A {
public:
	A() { std::cout << "A defualt construct" << '\n'; }
	A(const B& b) : _b(b) { 
		puts("---------------------");
		std::cout << "A construct" << '\n';
	}
	/*A(const B& b){ 
		puts("---------------------");
		_b = b;
		std::cout << "A construct" << '\n'; 
	}*/

	A(const A& a) : _b(a._b) { std::cout << "A copy construct" << '\n'; }
	A& operator=(const A& a) {
		_b = a._b;
		std::cout << "A assign operator" << '\n';
		return *this;
	}
private:
	B _b;
};
int main() {
	B b(1);
	A a(b);
}

1.2 必须在初始化列表初始化的成员

• const修饰的成员变量

• 引用类型成员

• 类类型成员,且该类没有默认构造函数(由1.1内容可得)

2 引用&引用与指针的区别

2.1 引用初始化以后不能被改变,指针可以改变所指的对象

int main() {
	int a = 10;
	int& ref = a;     
	int b = 20;    
	ref = b;
	std::cout << "a:" << a << " ref:" << ref << " b:" << b; 
     //output:a:20 ref:20 b:20
}

 2.2 引用和指针的区别

引用和指针,下面说法不正确的是()

A. 引用和指针在声明后都有自己的内存空间

B. 引用必须在声明时初始化,而指针不用

C. 引用声明后,引用的对象不可改变,对象的值可以改变,非const指针可以随时改变指向的对象以及对象的值

D. 空值NULL不能引用,而指针可以指向NULL

答案:A

#include <iostream>

int main() {
	int a = 10;
	int& ref = a;
	std::cout << "a:" << &a << '\n' << "ref:" << &ref << '\n';
	//a:00FCF8D4     ref:00FCF8D4

	int b = 10;
	int* ptr = &b;
	std::cout << "b:" << &b << '\n' << "ptr:" << &ptr << '\n';
	//b : 00FCF8BC     ptr: 00FCF8B0

	return 0;
}

 从定义内存上看,引用和被引用变量公用同一块空间

3 构造函数与析构函数系列题

3.1构造函数与析构函数的调用次数

1)

C++语言中,类ClassA的构造函数和析构函数的执行次数分别为()

ClassA *pclassa=new ClassA[5];
delete pclassa;

A. 5,1

B. 1,1

C. 5,5(错误)

D. 1,5

答案:A 

2)

#include <iostream>
#include <string>
using namespace std;
class Test {
public:
	Test(){ std::cout << this << "B defualt construct" << '\n'; }
	~Test() { std::cout << this <<   "B destory" << '\n'; }
};
int main() {
	Test t1;
	puts("------------");
	Test* t2;
	puts("------------");
	Test t3[3];
	puts("------------");
	Test* t4[3];        //t4是存放三个类型Test*的对象的数组
	puts("------------");
	Test(*t5)[3];       //t5是数组指针,指向一个存放三个类型为Test的对象的数组
	puts("------------");
}

 打印结果:

4 类的运算符重载

在重载一个运算符为成员函数时,其参数表中没有任何参数,这说明该运算符是 ( )。

A. 无操作数的运算符

B. 二元运算符

C. 前缀一元运算符

D. 后缀一元运算符(错误)

答案:C

例如:

前置++:T& operator++() {} 

后置++:T operator++(int) {}

5 类的静态数据成员

下面有关c++静态数据成员,说法正确的是()

A. 不能在类内初始化(错误)

B. 不能被类的对象调用

C. 不能受private修饰符的作用

D. 可以直接用类名调用  

答案:D : 

知识点:const修饰的静态成员可以在类内初始化,所以A错误

5.1 malloc/new/new[]

malloc/calloc/realloc <----> free        new <----> delete        new [] <----> delete[]三者一定要匹配使用,否则会产生内存泄漏或者程序崩溃

5.2 new的实现步骤与细节

1) 对于 T*p = new T;

-第一步: 调用operator new(size_t size)申请空间(内部调用malloc循环申请)

-第二步: 调用构造函数完成对申请空间的初始化

     对于 delete p;

-第一步:调用析构函数释放p指向的对象中的资源

-第二步:调用operator delete释放p所指向的空间(内部调用free)

2)对于 T*p = new T[N];

-第一步: 调用operator new[](size_t size)申请空间(内部调用operator new(size_t size))

-第二步: 调用N次T的构造函数完成对申请空间的初始化

     对于 delete p;

-第一步:调用N次T的析构函数释放p指向的N个对象中的资源

-第二步:调用operator delete[]释放p所指向的空间(内部调用operator delete)

6 this指针相关题目 

6.1 this指针存放在哪里?

this指针是对象调用自身方法时的第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,通过下面三张图就可以自然的理解到this指针

 第一张图可以看到,寄存器ecx存放的值是对象t的起始四个字节的地址

指令lea:加载有效地址(load effective address)指令就是lea,他的指令形式就是从内存读取数据到寄存器,但是实际上他没有引用内存,而是将有效地址写入到目的的操作数,就像是C语言地址操作符&一样的功能,可以获取数据的地址。

callfunc1或者func2之前会将对象t的起始四个字节的地址加载到ecx寄存器

 第二张图,打印对象t的地址,调用func2,将n的值对象成员n的值改为1

调用函数,将eax寄存器push如栈,此时在监视窗口可以看到this指针和ecx的值相同,我的理解是this是函数的隐含参数,函数调用时创建,其值为eax的值,也就是最开始存放在eax中的对象的起始地址;

 6.2 this可以为空吗?

答案是可以的,我们之前说到,this指针的值是对象的起始地址,如果当对象的地址为nullptr时,那么this就为空。

但是这是对this的解引用或者访问成员操作就是错误的。

函数func1调用完成没有异常,func2函数内部异常

6.3 delete this 以及 delete细节解析

如果有一个类是 myClass , 关于下面代码正确描述的是:

myClass::~myClass(){
    delete this;
    this = NULL;
}

A. 正确,我们避免了内存泄漏

B. 它会导致栈溢出

C. 无法编译通过                            

D. 这是不正确的,它没有释放任何成员变量。(错误) 

答案:C

对于上述代码,首先它是不能被编译通过的,因为this指针本身被const修饰(对于上述例子而言this指针的类型为myClass *const), this指针本身无法被修改

如果删去`this = NULL`这一段代码,程序还是有错,我们通过下面几个例子说明⬇️

首先我们需要了解:调用delete函数之后会依次执行下面两个步骤 

① 对目标调用的析构函数

② 调用operator delete释放内存

通过下面几种了解:

1)

#include <iostream>
using namespace std;

class Test {
public:
	Test() {
		puts("Test()");
		x = 0;
		ptr = new int(0);
	}
	~Test() {
		puts("~Test() before");
		delete this;
		//this = nullptr;   //编译错误	C2106“ = ”: 左操作数必须为左
		puts("~Test() after");

	}
private:
	int x;
	int* ptr;
};

int main() {
	Test t;
}

 上面这段代码执行会不断打印~Test() before,直至程序栈溢出

解释了调用operator delete之后的执行步骤,上述代码会this指针指向对象的析构函数,而析构函数中又有delete函数,导致死循环,如下图⬇️

2)

#include <iostream>
using namespace std;

class Test2 {
public:
	Test2() {
		ptr = new int(0);
	}
	~Test2() {
		puts("~Test2");
		delete ptr;
		ptr = nullptr;
	}
	void deletefunc() {
		delete this;   //先析构,再delete this指向的堆空间(当this指向的是栈上的空间时,程序崩溃)
	}
private:
	int* ptr;
	int x = 0;
};
int main() {
	Test2* tptr = new Test2();
	tptr->deletefunc();
}

通过上述代码和动画演示巩固delete的两个步骤;

如过将对象创建再栈中,上述程序又会出现bug:编译阶段不会报错,但是再运行到delete this的时候程序崩溃了,原因是对栈上的空间进行了释放

	Test2 obj = Test2();
	obj.deletefunc();

3)

#include <iostream>
using namespace std;

void operator delete(void* ptr) {     
	puts("operator delete");
}
class Test2 {
public:
	Test2() {
		ptr = new int(0);
	}
	~Test2() {
		puts("~Test2");
		delete ptr;
		ptr = nullptr;
	}
	void deletefunc() {
		delete this;   
	}
private:
	int* ptr;
	int x = 0;
};
int main() {
	Test2* ptr = new Test2();
	ptr->deletefunc();
}

调试上述代码

7. 继承与多态

几乎所有题目知识点这篇文章有覆盖,即使复习!!!

⌈C++⌋从无到有了解并掌握C++面向对象三大特性——封装、继承、多态-CSDN博客

7.1 虚析构函数

下面说法正确的是()

A. 一个空类默认一定生成构造函数,拷贝构造函数,赋值操作符,取地址操作符,析构函数

B. 可以有多个析构函数

C. 析构函数可以为virtual,可以被重载(错误)

D. 类的构造函数如果都不是public访问属性,则类的实例无法创建

答案:A

析构函数可以是虚函数,但是由于只能由一个析构函数,所以自然不存在重载 

知识点:重载、覆盖、隐藏的区别

7.2 纯虚函数

1)

以下关于纯虚函数的说法,正确的是()

A. 声明纯虚函数的类不能实例化

B. 声明纯虚函数的类成虚基类

C. 子类必须实现基类的纯虚函数(错误)

D. 纯虚函数必须是空函数

答案:A 

具体知识点在《⌈C++⌋从无到有了解并掌握C++面向对象三大特性——封装、继承、多态》的第三章的第三节

含有(或者未经覆盖直接继承)纯虚函数的类是抽象基类(abstract base class)。抽象基类负责定义接口(因此又叫做接口类),而后续的其他类可以覆盖该接口。   (C++ Primer   P540)

抽象基类(或未覆盖纯虚函数直接继承的派生类)无法实例化出对象

2)

关于抽象类和纯虚函数的描述中,错误的是

A. 纯虚函数的声明以“=0;”结束

B. 有纯虚函数的类叫抽象类,它不能用来定义对象 

C. 抽象类的派生类如果不实现纯虚函数,它也是抽象类(错误答案)

D. 纯虚函数不能有函数体

答案:D        纯虚函数是可以由函数体的 

#include <iostream>
using namespace std;
class A {
public:
	virtual void fun() = 0 {
		puts("A:virtual void fun() = 0");
	}
};

class C : public A {
public:
	virtual void fun() {
		puts("C:virtual void fun()");
	}
};
int main() {
	C c;
	c.fun();    //output:  C:virtual void fun()
	return 0;
}

7.3 继承与组合

具体知识点在《⌈C++⌋从无到有了解并掌握C++面向对象三大特性——封装、继承、多态》的第二章的第7节

面向对象设计中的继承和组合,下面说法错误的是?()

A. 继承允许我们覆盖重写父类的实现细节,父类的实现对于子类是可见的,是一种静态复用,也称为白盒复用

B. 组合的对象不需要关心各自的实现细节,之间的关系是在运行时候才确定的,是一种动态复用,也称为黑盒复用(错误)

C. 优先使用继承,而不是组合,是面向对象设计的第二原则

D. 继承可以使子类能自动继承父类的接口,但在设计模式中认为这是一种破坏了父类的封装性的表现

答案:C

7.4 继承体系中的构造与析构顺序

7.4.1 基类部分隐式销毁

具体知识点在《⌈C++⌋从无到有了解并掌握C++面向对象三大特性——封装、继承、多态》的第二章的第5.4小节

C++将父类的析构函数定义为虚函数,下列正确的是哪个()

A. 释放父类指针时能正确释放子类对象

B. 释放子类指针时能正确释放父类对象

C. 这样做是错误的

D. 以上全错

答案:A 

知识点:

在析构函数题执行完成后,对象成员会被隐式销毁。类似的,对象的基类部分也是隐式销毁的。因此,和构造函数及赋值运算符不同的是,派生类析构函数只负责销毁由派生类自己分配的资源    (C++ Primer   P556)

为什么先析构子类再析构父类?如果先析构父类会怎么样?

如果先析构父类,父类析构后,若子类再析构之前需要访问父类成员访问的则是一个空指针;而先析构子类则没有这个风险,因为父类不能访问子类成员

 7.4.2 多继承构造顺序

由下图可知:

1)

 2)

下面这段代码会打印出什么? 

class A
{
public:
	A()
	{
		printf("A ");
	}
	~A()
	{
		printf("deA ");
	}
};
class B
{
public:
	B()
	{
		printf("B ");
	}
	~B()
	{
		printf("deB ");
	}
};
class C : public A, public B
{
public:
	C()
	{
		printf("C ");
	}
	~C()
	{
		printf("deC ");
	}
};
int main()
{
	A* a = new C();
	delete a;
	return 0;
}

A. A B C deA

B. C A B deA

C. A B C deC(错误答案)

D. C A B deC

 答案:A 

8 其他于类相关的题目

8.1 空类的大小

在Windows 32位操作系统中,假设字节对齐为4,对于一个空的类A,sizeof(A)的值为()? A. 0

B. 1

C. 2

D. 4(错误)

答案:B

类大小的计算方式:与结构体大小的计算方式类似,将类中非静态成员的大小按内存对齐规则计算,并且不用计算成员函数;

特别的,空类的大小在主流的编译器中设置成了1

8.2 对const变量的修改

以下程序输出是____。

#include <iostream>
using namespace std;
int main(void)
{
 const int a = 10;
 int * p = (int *)(&a);
 *p = 20;
 cout<<"a = "<<a<<", *p = "<<*p<<endl;
 return 0;
}

A. 编译阶段报错运行阶段报错

B. a = 10, *p = 10

C. a = 20, *p = 20(错误)

D. a = 10, *p = 20

E. a = 20, *p = 10

 答案:D

知识点:

1)编译器在编译阶段会对const修饰的变量进行优化,将其替换成变量的值

由图中的汇编代码可以看到,打印变量a时,他被直接替换成了10这个常量

  volatile

C/C++ 中的 volatile 关键字和 const 对应,用来修饰变量,volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。

#include <iostream>
using namespace std;
int main(void)
{
	const int volatile a = 10;
	int* p = (int*)(&a);
	*p = 20;
	cout << "a = " << a << ", *p = " << *p << endl;
	return 0;
}

当用volatile修饰a之后打印结果为:

 8.3 赋值运算符重载

下列关于赋值运算符“=”重载的叙述中,正确的是

A. 赋值运算符只能作为类的成员函数重载

B. 默认的赋值运算符实现了“深层复制”功能

C. 重载的赋值运算符函数有两个本类对象作为形参(错误)

D. 如果己经定义了复制拷贝构造函数,就不能重载赋值运算符

答案:A

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Dusong_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值