牛客网C++选择题选讲
1. switch的本质
#include "stdio.h"
int main() {
int c = 0,k;
for(k = 1;k < 3;k++) {
switch (k) {
default : c += k;
case 2: c++;
break;
case 4: c += 2;
break;
}
}
printf("%d\n", c);
return 0;
}
以下程序的输出结果是3。
解析:switch
的规则是
- 首先寻找匹配的
case
,若找不到就匹配default
(即使default
写在前面) - 然后忽略掉其他所有标签,从匹配到的标签开始一直执行到末位或break处(这就是可以叠加
case
的原因和一般要写break
的原因)
如果没有匹配到case
又没有default
则不执行。实际编码中不应像上文这么写,如果有default
应该写最后。
2. 只能内重载的运算符
下列关于赋值运算符“=”重载的叙述中,正确的是
A. 赋值运算符只能作为类的成员函数重载
B. 默认的赋值运算符实现了“深层复制”功能
C. 重载的赋值运算符函数有两个本类对象作为形参
D. 如果己经定义了复制拷贝构造函数,就不能重载赋值运算符
答案为A。解析::: * . ?:
是特殊的不能重载的运算符,而更特殊一类运算符的是= [] () ->
,他们可以重载,但是必须重载在函数内部,不允许重载为外部友元函数,即CLASS& operator=(CLASS &x, CLASS &y)
这样的形式是不被允许的。
3. 常量字符串的赋值
const char *p1 ="hello";
char *const p2 = "world";
给出以下定义,下列哪些操作是合法的?
A.
p1++;
B.
p1[2] = 'w';
C.
p2[2] = 'l';
D.
p2++;
选A。解析:首先我们要理解顶层和底层const,const*
代表指针指向的值不能修改,*const
代表指针自身不能被修改,因此B和D错误。而一个字面量字符串其实是一个常量字符数组,被单独存放在只读区,他虽然可以赋值给没有顶层const
的指针,但是却仍然不可修改,因此C错误。在实际测试中,C选项会给出警告,并在运行时发生错误。
4. 传指针却不修改值
#include <stdio.h>
#include <stdlib.h>
void fun(int *p1, int *p2, int *s) {
s = (int *)malloc(sizeof(int));
*s = *p1 + *(p2++);
}
int main() {
int a[2] = {1, 2}, b[2] = {10, 20}, *s = a;
fun(a, b, s);
printf("%d \n", *s);
}
答案为1,不是11。虽然s作为传入了函数,但s自己不是引用,仍是按值传递,因此改变的s,与main里的s没有关系。
5. dynamic_cast的用法
struct A1{
virtual ~A1(){}
};
struct A2{
virtual ~A2(){}
};
struct B1 : A1, A2{};
int main()
{
B1 d;
A1* pb1 = &d;
A2* pb2 = dynamic_cast<A2*>(pb1); //L1
A2* pb22 = static_cast<A2*>(pb1); //L2
return 0;
}
A. L1语句编译失败,L2语句编译通过
B. L1语句编译通过,L2语句编译失败
C. L1,L2都编译失败
D. L1,L2都编译通过
选B。static_cast
用于普通的类型转换,但是pb1是A1的指针,A1和A2之间没有继承关系,所以转换不被允许。而dymamic_cast
是运行时转换,一定可以通过编译;在实际运行时,程序检测pb1的实际类型(这里是B1)能否通过继承关系转化为A2*(在这里可以),若存在关系则转换,否则返回空指针。
当然我们知道所有指针大小是一样的,如果非要在编译期转换,可以使用C风格强制转换A2* pb22 = (A2*)pb1
。
6. 局部变量的引用
std::string& test_str(){
std::string str = "test";
return str;
}
int main(){
std::string& str_ref = test_str();
std::cout << str_ref << std::endl;
return 0;
}
局部变量返回后即销毁,这样的写法是不对的,但是实际上可以通过编译,只会给出警告,并在运行期发生错误。C++秉承“相信程序员”的原则不做检查,我们只能自己小心。
7. 友元声明问题
#include <math.h>
#include <iostream>
using namespace std;
class Point
{
friend double Distance(const Point &p1, const Point &p2) /* ① */
{
double dx = p1.x_ - p2.x_;
double dy = p1.y_ - p2.y_;
return (sqrt(dx * dx + dy * dy));
}
public:
Point(int x, int y) : x_(x), y_(y) /* ② */
{
}
private:
int x_;
int y_;
};
int main(void)
{
Point p1(3, 4);
Point p2(6, 9);
cout << Distance(p1, p2) << endl; /* ③ */
return (0);
}
请问程序会在哪个地方发生编译错误?答案是不会。虽然Distance定义在类中,但他不是成员函数,是一个外部函数。
但是这道题有不合理的地方,友元声明和声明是不一样的,首先来说一说规范写法:
-
类中的
friend double Distance(const Point &p1, const Point &p2);
是类中的“友元声明”,表示“我把秘密告诉你”。 -
类外的
double Distance(const Point &p1, const Point &p2);
,是函数自己的声明。 -
具体定义提供在声明后。
“友元声明”只是表明友元关系,与外部的声明是分离的。因此,一般来讲,即使向上面一样将定义放在友元声明后,也需要在类外再声明一次。
但是上面的代码没有这样做,这是因为另一个例外:如果这个函数的参数带有这个类,则可以不用声明。上例中Distance带有的Point参数,如果把他改成空参数,就不再能通过编译了。
实际编程中应该避免这种“例外”的发生。友元本就不属于类,应该写在外面;况且“不用声明”这个特性并非标准,某些编译器并不支持。
8. 继承类的默认构造函数
template<class T> class Foo{
T tVar;
public:
Foo(T t) : tVar(t) { }
};
template<class T> class FooDerived:public Foo<T>
{
};
int main()
{
FooDerived<int> d(5);// 1
FooDerived<int> d;// 2
return 0;
}
1,2两处都无法通过编译。Foo类定义了一个构造函数,因此不再有无参的默认构造函数,FooDerived类没有自定义构造函数,因此会合成一个默认构造函数,该函数又会自动调用Foo类的默认构造函数,但是这个函数不存在,所以不能通过编译。
Tip:定义一个类时,对于构造拷贝赋值析构这一系列函数,要么一个都不写,要么就整个系统都要写上,可以避免所有问题,不要把时间浪费在这种事情的思考上
9. 虚函数与结构体对齐
class A {
virtual void func() {
cout << "func" << endl;
}
float f; char p; int adf[3];
};
sizeof(A)
是多少呢?答案是:若平台指针占4字节则答案为24,占8字节则是32。以24为例,对于有虚函数的类,每个类中都会生成一个“虚函数指针”,指针指向这个类的虚函数列表。之后是占4字节的float,占1字节但填满4字节的char,和3个int占12字节。
虚函数指针指向的是这个类的虚函数列表,便于运行时高效确定调用哪一个函数,更多内容请见:知乎 c++虚指针和虚函数表
10. 大端模式和小端模式
unsigned int a = 0x12345678;
unsigned char b = *(unsigned char *)&a;
这时b的值是多少呢?答案是未定义的。表达式相当于取a的前一半赋值给b,那么b是0x12还是0x78?这都是可以的。在储存数据时,既可以顺着字节高低顺序内存中的四个字节为0x12 0x34 0x56 0x78
(此时b为0x12
),也可以反过来排列为0x78 0x56 0x34 0x12
(此时b为0x78
)。前者叫做大端模式,后者叫做小端模式。容易发现小端模式截断时不需要移动,大端模式要截断则需要额外移动。
实际表现是大端还是小端取决于硬件。更多内容:博客园 详解大端模式和小端模式
11. 枚举的大小
#include <iostream>
using namespace std;
typedef enum
{
Char ,
Short,
Int,
Double,
Float,
}TEST_TYPE;
int main() {
TEST_TYPE val;
cout<< sizeof(val)<<endl;
return 0;
}
答案为4。enum
里的描述实际上是描述它可能有哪一些取值,不是有多少量在里面,无论写多少个,枚举量所占的大小都是固定不变了,在默认情况下相当于一个int
(即4)。在C++11以后可以手动指定枚举的类型:
enum A: unsigned long long {
X, Y, Z;
};
12. 位域的大小
struct {
unsigned int a:2;
unsigned int b:1;
} f;
此时sizeof(f)
是4。'a:2'表示给a分配两个位,类似一般结构体的对齐,这里给a分配了2位,b分配了1位,剩下29位没有用到,一共32位。
一个位域相当于不是整字节数的量,相邻的位域会挤在一个字节里,若不够则则继续开,每次开的大小即出现的最大类型大小。
13. BSS段
char s1[100];
int s2 = 0;
static int s3 = 0;
void main() {
char s4[100];
}
以下变量分配在BSS段的是(s1)。
- BSS段:通常是指用来存放程序中未初始化的全局变量的一块内存区域;
- 数据段:通常是指用来存放程序中 已初始化 的 全局变量 的一块内存区域,static意味着在数据段中存放变量;
- 代码段:通常是指用来存放 程序执行代码 的一块内存区域;
- 堆:存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减,这一块在程序运行前就已经确定了;
- 栈又称堆栈, 存放程序的 局部变量 (不包括static声明的变量)。除此以外,在函数被调用时,栈用来传递参数和返回值。
14. 多重指针的多重const
含有const的多重指针是如何工作的?直接摘录牛客网大佬的:
int main()
{
const int x = 1;
int* p;
int const** q = &p; //① q为一个指向const int*的指针,现在将其指向non-const int* p
*q = &x; // ②现在将q指向的指针赋为常量x的地址(*q即为指针p的地址,这时p指向x)
//**q=2; //③报错
*p = 2; //④因为p是non-const int *,所以可以对其赋值,这时将常量x的值改为了2,明显不符合常识
printf("%d",x);
}
再来理一遍:q是指向const int *
的指针,q自身没有常量属性,将他赋值为一个int *
,然后再把*q
(即p)赋值为x的地址,此时将通过p修改x的值,这是不合理的。
因此,C++中不允许将int**
赋值给const int**
(C中可以),这将导致常量被绑定到非常量指针上。若要实现预想中的功能,可以将int**
赋值给const int * const *
,表示“指向const int *
常量的指针”,此时上面代码*q = &x
将报错,因为不能通过q修改*q。
15. STL容器的分类
STL容易是可以分类的。
-
顺序容器:vector、deque、list、forward_list,他们是线性结构,前两个是顺序表,后两个是链表。
-
关联容器:set,map,multiset,multimap(以及unordered版本),他们不是线性结构,用平衡树或散列表实现。bitset也算特殊的关联容器
-
容器适配器:stack、queue、priority_queue,他们基于“vector”并更改了一些功能。但是你也可以在声明中选择用其他结构替代vector。实现链表队列等功能。
-
string形式上相当于变长char数组,但是它不是模板,因此不属于容器。
16. 重载函数隐藏
#include <iostream>
using namespace std;
class A {
public :
void run(void) {
cout << "run()" << endl;
}
void run(int a){
cout << "run(A)" << endl;
}
};
class B : public A {
public :
void run(int a) {
cout << "run(B)" << endl;
}
};
int main(void){
B b;
b.run(0);//语句1
b.A::run(1); //语句2
b.run(); //语句3
b.A::run(); //语句4
return 0;
}
此时哪个语句会报错?答案是只有语句3。如果不是虚函数,B中的run会覆盖掉A中的run,即使A中有多个重载的run,他们都会被覆盖,要调用A中函数只能显式调用。
17. 虚函数的默认参数
考虑下面的代码,调用哪个函数?输出什么?
class A
{
public:
virtual void func(int val = 1)
{ std::cout<<"A->"<<val <<std::endl;}
virtual void test()
{ func();}
};
class B : public A
{
public:
void func(int val=0)
{std::cout<<"B->"<<val <<std::endl;}
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
答案是,调用B::func,但是输出"B->1",这是因为C++中,虚函数可以动态绑定,但是默认参数还是静态的,test函数是A中的,因此调用B中func时仍然传递A中的默认参数0!
这样的语法耗子药没人喜欢,因此请避免在虚函数中使用默认参数!
18. 多继承虚指针布局
答案为A。对于含有虚函数的多继承来说,指针经过转化后不一定相等了,对于有包含虚指针的多继承类,他的虚指针和第一个基类对齐。因此指针转化为第一个基类后不变,转化成其他基类后变化。
19. 各种alloc的区别
- malloc:申请一块给定大小的堆中的内存(不会初始化),最常用,失败返回NULL
- calloc:申请n块给定大小的空间(但是时连在一起的,其实还是一块),并且做初始化,失败返回NULL。
- realloc:用于malloc和calloc申请的内存,进行扩容到指定大小,有可能失败返回NULL,有可能成功,返回原地址,也有可能返回新地址,此时原地址不会释放。
- alloc:少见。申请栈内存,不用释放。
20. 特定多态和通用多态
多态是面对对象的三个特性之一,不过多态这个词本身不限于此,只要是“一种函数功能,多种实现”的思想,都可以称之为多态。
C++中多态的形式可以表现为4种,搬运牛客网大佬的:、
//1.参数多态
//包括函数模板和类模板
//2.包含多态 virtual
class A{
virtual void foo() {printf("A virtual void foo()");}
};
class B : public A {
void foo() {printf("B void foo()");}
};
void test() {
A *a = new B();
a->foo(); // B void foo()
}
//3.重载多态
//重载多态是指函数名相同,但函数的参数个数或者类型不同的函数构成多态
void foo(int);
void foo(int, int);
//4.强制多态
//强制类型转换
重载多态和强制多态称为特定多态。
参数多态和包含多态称为通用多态。
21. 阻止直接创建对象
在C++中,为了让某个类只能通过new来创建(即如果直接创建对象,编译器将报错),应该()
答案:将析构函数声明为private。此时不允许直接在栈中创建对象,但是可以用new创建对象
22. fork()函数
for (int i = 0; i < 2; i++)
{
fork();
printf("-\n");
}
这个程序会输出几个-?答案是6个。fork()系统调用是Unix下以自身进程创建子进程的系统调用,调用fork后,会将本进程复制一个一模一样的子进程同时执行,子进程可以继续fork,最终一共6个。
23. 线性容器
答案是A。线性容器中,如果删除元素,it++可能已经失效,这时应该使用erase的返回值,erase会返回删除掉元素的下一个元素对于的迭代器。
不过实际中要避免这种情况,使用了线性表容器,就代表你不在中间插入或删除元素。
24. 异质链表的意义
异质链表就是每个节点类型不同的链表。在C++中,用到“基类指针指向派生类的特性”,只要让所有节点继承自同一个基类,我们就可以用基类的链表实现异质链表。链表指针都是基类形式,内容则可以是各种派生类。
25. 函数返回时构造函数的调用
在生成返回值的过程中,return rhs
返回一个新的临时对象,该对象被返回到main处,然后执行拷贝赋值,然后临时对象被销毁,因此选D。
26. C++不是类型安全的语言
在C++中,可以把0当成bool类型的false,也可以当做int中的数字0.则表示C++不是类型安全语言。C风格字符串和std::string的转化等等都相当随意,因此认为它不是类型安全的语言
27. 避免使用虚函数的情况
构造函数不应定义为虚函数,创建派生类对象的时候,应该直接调用派生类构造函数,它会先构造基类,然后再构造自身
析构函数可以定义成为虚函数,这样通过基类指针释放对象时才能正确调用。
构造函数内部不应该使用虚函数。虚函数调用哪一个是动态绑定的,但是此时类还没有构造完成,创建一个派生类对象将调用派生类的构造函数,派生类的构造函数将先使用基类的构造函数,如果基类的构造函数中使用虚函数,将会调用未初始化的派生类。如果你非要调用虚函数,他会直接调用基类里的函数,而不是按照通常的规则。
析构函数也同理,不应调用虚函数,基类调用虚函数时,派生类已经被销毁了。如果非要调用,他会调用基类里的函数
28. volatile的特性
volatile会限制该变量访问时的操作顺序,保证其不被编译器优化,volatile和volatile变量之间的操作是不会被优化的。
29. memcpy和memmove
这两个函数都是C风格的拷贝函数,memmove虽然名字为“移动”,其实仍然执行拷贝操作,不同之处是memmove保证在源区间和目标区间重叠时仍能正确处理(相应的开销更大),而memcpy不可以。
30. this指针的常量属性
指针有两种const。对于成员函数的this指针来说,他们都是常量指针(相当于Myclass *const this),因此成员函数内不允许改变this的值。
如果成员函数后面也加上了const,如void f() const{}
,则表明对象不可修改,相当于传入指向常量的指针(Myclass const*const this),此时this和*this(对象本身)都不允许修改
那么能不能delete this
呢?const只限制修改不限制销毁,所以是可以的。此时会调用析构函数销毁对象(所以析构函数里不应这么写,否则就会无限递归)。
31. 派生类的构造顺序
构造Derived对象的输出顺序为CBAD。调用顺序于冒号后面的顺序无关,而是按照下面的顺序:基类>自身的class对象>自身的内置对象。对象按声明顺序,由于B声明在前,所以先是B。