牛客面试题记录

1.初始化成员列表

《C++ Primer》中提到在以下三种情况下需要使用初始化成员列表:
情况一、需要初始化的数据成员是对象的情况(这里包含了继承情况下,通过显示调用父类的构造函数对父类数据成员进行初始化);
情况二、需要初始化const修饰的类成员;c++11以后const是可以不用在初始化列表初始化的
情况三、需要初始化引用成员数据;

2.static成员变量

  • 可在类定义内声明,但是必须在类外初始化。为了避免多文件同时引用同一个头文件时,重复定义的问题,比如静态全局变量是全局唯一的。
  • 非静态成员函数也可以操作静态数据成员
  • 全局变量、静态全局变量和静态局部变量都存放在内存的静态存储区域,局部变量存放在内存的栈区。

    在创建实例对象的时候,内存中会为每一个实例对象的每一个非静态成员变量开辟一段内存空间,用来存储这个对象所有的非静态成员变量值

  • static类变量是所有对象共有,其中一个对象将它值改变,其他对象得到的就是改变后的结果

3.指针数组和数组指针

int *target[5]:指针数组,每个元素都是一个指向int*的指针;
int (*target)[5]:数组指针,相当于二维数组
 
4.虚函数

 

 

虚函数:在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数。
虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。

虚析构函数的作用是delete动态对象时释放资源。若析构函数不为虚,则指向派生类对象的基类指针在delete时无法释放派生类对象,造成内存泄漏

 
A构造函数执行时还未创建对象,此时还没有虚函数表。
B将基类的析构函数声明为虚函数,delete一个指向子类对象的基类指针,实际被执行的是子类的析构函数,而子类的析构函数会自动的调用基类的析构函数,进而保证所有资源都可以释放,防止内存泄露。
C虚函数不可内联
D静态成员函数属于类,不属于特定对象,既不会通过虚函数来调用。因为虚函数表位于特定的对象之中
 
5. 在派生类的函数中,能够直接访问全部基类的公有成员和保护成员。这句话是否正确?
直接基类可以,间接基类不可以。
题目没说是直接基类还是间接基类。
6.拷贝构造函数

 

 

 

 

 

 拷贝构造函数调用时机

 

 

 

 将类的一个对象赋值给该类的另一个对象时,调用的不是拷贝构造函数,而是赋值运算符重载函数。

 

#include <stdio.h>
class A
{
public:
    A()
    {
        printf("1");
    }
    A(A &a)
    {
        printf("2");
    }
    A &operator=(const A &a)
    {
        printf("3");
        return *this;
    }
};
int main()
{
    A a;
    A b = a;
}//12
A a,定义一个对象,毫无疑问调用构造函数
A b=a,这是定义了对象b,且以a对b进行初始化,这个时候需要调用拷贝构造函数。
如果写成A a;A b;b=a;则是调用后面重载的赋值函数,这种情况应该输出113。
这个题主要考察赋值和初始化的区别。
7.方法的重载是同一个类中方法之间的关系,是水平关系。
8. 基类指针(p)指向子类对象(d), 所以通过p用->调用函数时, 是虚函数则调用子类d的, 不是虚函数则调用基类p的
#include <iostream>
using namespace std;

class Base {
public:
    virtual void f() {
        cout << "f0+";
    }
    void g() {
        cout << "g0+";
    }
};
class Derived : public Base {
public:
    void f() {
        cout << "f+";
    }
    void g() {
        cout << "g+";
    }
};

int main() {
    Derived d;
    Base* p = &d;
    p->f();
    p->g();
    return 0;
}

//f+g0+

 9.执行顺序

1.构造函数:用来初始化某个对象,一个类可以有多个构造函数
析构函数:在对象完成其作用域后,清理这个对象内存空间,一个类只有一个析构函数
2.继承关系的构造函数执行顺序:先执行父类(基类)的构造函数,再执行子类的构造函数
继承关系的析构函数执行顺序:先执行子类的析构函数,再执行父类(基类)的析构函数(与构造函数执行顺序相反)
 
10.字节数
unsigned short类型占个字节
二进制下最大值为111111111111111
转为十进制就是65535  
故取值范围是[0,65535]
short 【int】有符号短整型,数值范围为:-32768~32767;
unsigned short【int】无符号短整型,数值范围为:0~65535;
其余的一些常用的数据类型的数据范围
int 有符号基本类型,数值范围为::-32768~32767。 
[signed] long [int]有符号长整型,数值范围为:-2147483648~2147483647。
unsigned int 无符号基本整型,数值范围为:0~65535。
unsigned long【int】无符号长整型,数值范围为: 0~4294967295。
 
11.二进制算法
  • 二进制的【或】运算:遇1得1
    参加运算的两个对象,按二进制位进行“或”运算。
    运算规则:0|0=0; 0|1=1; 1|0=1; 1|1=1;
    参加运算的两个对象只要有一个为1,其值为1。
    例如:3|5 
    0000 0011
    0000 0101
    0000 0111

  • 二进制的【与】运算:遇0得0
    运算规则:0&0=0; 0&1=0; 1&0=0; 1&1=1;
    即:两位同时为“1”,结果才为“1”,否则为0
    例如:3&5
    0000 0011
    0000 0101
    0000 0001

  • 二进制的【非】运算:各位取反
    运算规则:~1=0; ~0=1;
    对一个二进制数按位取反,即将0变1,1变0。

  • 二进制的【异或】运算符 “^”:相同为0 ,不同为1”
    参加运算的两个数据,按二进制位进行“异或”运算。
    运算规则:0^0=0; 0^1=1; 1^0=1; 1^1=0;
    参加运算的两个对象,如果两个相应位为“异”(值不同),则该位结果为1,否则为0。

12.类的访问权限

    • public:可以被该类中的函数、子类的函数、友元函数访问,也可以由该类的对象访问;
    • protected:可以被该类中的函数、子类的函数、友元函数访问,但不可以由该类的对象访问;
    • private:可以被该类中的函数、友元函数访问,但不可以由子类的函数、该类的对象、访问。

 13.const作用

  • 和指针一起使用

Const可以和指针一起使用,它们的组合情况比较复杂,可归纳为三种:指向常量的指针,常指针和指向常量的常指针。

(1) 指向常量的指针是指一个指向常量的指针变量,例如:

const char* info = “name”;          // 声明指向常量的指针

这条语句的含义是:声明一个名为info的指针变量,它指向一个字符型的常量,初始化为info指向字符串“name”。

所以下面的一句是错误的:info [3] = ‘b’;

因为这里指针所指的数据(name)是常量,不能通过解引用来修改该数据。

然后下面这句话是允许的:info = “sex”;

因为指针本身是变量,可以指向其他的内存单元。

(2) 常指针是指把指针本身,而不是它指向的对象声明为常量,例如:

char * const info = “name”;         // 常指针

这条语句的含义是:声明一个名为info的指针变量,它指向一个字符型数据的常指针,初始化为“name”的地址。常指针就是创建一个不能移动的固定指针(地址不能改变),但是它所指的数据是可以改变的。

所以下面的一句是错误的:info = “sex”;

因为指针本身是常量,不能指向其他的内存单元。

然后下面这句话是允许的:info [3] = ‘b’;

因为指针所指向的数据是可以通过解引用来修改的。

char a[10] a实际上是指向char的常量指针,相当于char * const a; 变量a是只读的,read-only,不能再对其进行赋值操作

(3) 指向常量的常指针是指这个指针本身不能改变,它所指向的值也不能改变,声明一个指向常量的常指针,二者都要声明为const,例如:

const char * const info = “name”;

这里就是上面两种的结合了,info = “sex”和info [3] = ‘b’都是错误的。

其他说明:

  a. 常量一旦建立,在程序的任何地方都不能在修改。

  b. 与#define定义的常量不同,const定义的常量因为有自己的数据类型,这样C++编译时就会进行严格的类型检查,具有良好的编译时的检测性。

  c. 函数的参数也可以用const说明,用于保证实参在该函数内部不被改动。

  • 常引用

如果在声明引用是用const修饰,那么该引用就被称为常引用。常引用所引用的对象不能被更新。如果用常引用做为形参,便不会产生对实参不希望的修改。

定义:const 数据类型 &引用名;

如: int a = 5;

     const int &b = a;

其中,b是一个常引用,它所引用的对象不允许更改,如果出现:

b=6;

则是非法的。(如果你写a=6,这个不会出问题,因为a不是常类型变量。)

常引用常常被用作形参,例如:

void main()
{
    int a = 10;
    int b = 20;
    add(a, b);
}
 
int add(cons int &a, const int &b)
{
    // a += 10;         // 非法的,形参a为常引用,不能改变它的值
    return (a + b);
}
  • 常对象

如果在声明对象的时候用const修饰,则称被声明的对象为常对象。常对象的数据成员值在对象的整个生命周期内不能被改变。

定义:类名 const 对象名[(参数表)]或者const 类名 对象名[(参数表)]

在定义对象时必须进行初始化,而且不能被更新,C++不允许直接或间接的更改常对象的数据成员。

  • 常对象成员

(1) 常数据成员

类的数据成员可以是常量或常引用,使用const说明的数据成员称为常数据成员。如果在一个类中声明了常数据成员,那么构造函数就只能通过初始化列表对该数据成员进行初始化,而任何其他的函数都不能对该成员函数赋值。

(2) 常成员函数

在类中使用关键字const的函数称为常成员函数,常成员函数的格式如下:

返回类型 函数名(参数表)const;

const是函数类型的组成部分,所以在函数的实现部分也要带关键字const。

如:

void showDate() const;       // 声明
void showDate() const       // 实现
{
    printf(“year”);
}

说明:

  a. 如果将一个对象声明为常对象,则通过该对象只能调用它的常成员函数,而不能调用普通的成员函数。常成员函数是常对象的唯一的对外接口。

  b. 常成员函数不能更新对象的数据成员,也不能调用该类中的普通成员函数,这就保证了在常成员函数中绝对不会更新数据成员的值。

 

 

A.p是指针常量,*p是代表x的值,被定义为常量,不可再赋值,A错;
B.&q是对x变量的引用,但被定义为了常量,故q不再是变量,不能自增,B错;
C.如果const位于*的左侧,则const就是用来修饰指针所指向的变量,即指针指向为常量,故x为常量,不能改变,但是next指向x的地址,next++表示改变地址,故c选项无语法错误;
D.(*j)++表示x的值自增,但是const int *j=&x表示x为常量,不能改变,故d错误;
 
14. virtual 函数是动态绑定,而缺省参数值却是静态绑定。
记住:virtual 函数是动态绑定,而缺省参数值却是静态绑定。 意思是你可能会 在“调用一个定义于派生类内的virtual函数”的同时,却使用基类为它所指定的缺省参数值。
结论:绝不重新定义继承而来的缺省参数值!(可参考《Effective C++》条款37)
对于本例:
B*p = newB;
 
p->test();
p->test()执行过程理解:
       (1) 由于B类中没有覆盖(重写)基类中的虚函数test(),因此会调用基类A中的test();
       (2) A中test()函数中继续调用虚函数 fun(),因为虚函数执行动态绑定,p此时的动态类型(即目前所指对象的类型)为B*,因此此时调用虚函数fun()时,执行的是B类中的fun();所以先输出“B->”;
       (3) 缺省参数值是静态绑定,即此时val的值使用的是基类A中的缺省参数值,其值在编译阶段已经绑定,值为1,所以输出“1”;
       最终输出“B->1”。所以大家还是记住上述结论:绝不重新定义继承而来的缺省参数值!
15.内联函数
如果内联函数定义在调用函数的后面,则编译器会将其当作普通函数调用来看,并不会直接插入到调用处。
 
16.
a(4),一个对象a,调用1次构造函数;
b(5),一个对象b,调用1次构造函数;
c[3],三个对象c[0],c[1],c[2],调用3次构造函数;
*p[2],指针数组,其元素分别指向a和b,所以没有调用构造函数。
总共5次。
 
17.C++ 文件到生成exe文件需要经过预处理、编译、汇编和链接几个步骤。
预处理:在预处理阶段,编译器主要作加载头文件、宏替换、条件编译的作用。
编译:在编译过程中,编译器主要作语法检查和词法分析。可以通过使用 -S 选项来进行查看,该选项预处理之后的结果翻译成汇编代码。
汇编:在汇编过程中,编译器把汇编代码转化为机器代码。
链接:链接就是将目标文件、启动代码、库文件链接成可执行文件的过程。
 
18.C++容器 STL
在 C++ 中,容器是一种标准类模板STL:即Standard Template Library

什么是STL?

  • 算法包括排序,复制等常用算法,以及不同容器特定的算法。
  • 容器就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是set,map等。

  • 迭代器就是在不暴露容器内部结构的情况下对容器的遍历。

vector和list的区别?

vector和built-in数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随即存取,即[]操作符,但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝,另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了vector的效率。

list就是数据结构 中的双向链表(根据sgi stl源代码),因此它的内存空间可以是不连续的,通过指针来进行数据的访问,这个特点使得它的随即存取变的非常没有效率,因此它没有提供[]操作符的重载。但由于链表的特点,它可以以很好的效率支持任意地方的删除和插入。

vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随即存取

list拥有一段不连续的内存空间,如果需要大量的插入和删除,应该使用list

vector::iterator支持“+”、“+=”、“<”等操作符

list::iterator则不支持“+”、“+=”、“<”、“[]”等操作符运算

 

类模板的使用实际上是类模板实例化成一个具体的类

类模板:
            template<class T>
            class A {};
类模板的使用实际上是给定 T 之后确定一个新类。虽然不同T的类执行的功能一样,但是给定T的不同使得这些类里参数类型不同。
模板类是针对与类模板来说的。例如vector<int> vec;这是一个模板类实例化出一个对象vec
所以类模板的使用实际上是类模板实例化成一个具体的类
 
19. 派生类
1. public继承方式
        基类中所有 public 成员在派生类中为 public 属性;
        基类中所有 protected 成员在派生类中为 protected 属性;
        基类中所有 private 成员在派生类中不能使用。
2. protected继承方式
        基类中的所有 public 成员在派生类中为 protected 属性;
        基类中的所有 protected 成员在派生类中为 protected 属性;
        基类中的所有 private 成员在派生类中不能使用。
3. private继承方式
        基类中的所有 public 成员在派生类中均为 private 属性;
        基类中的所有 protected 成员在派生类中均为 private 属性;
        基类中的所有 private 成员在派生类中不能使用。
20.结构体对齐
struct _THUNDER {
    int iVersion;
    char cTag;
    char cAdv;
    int iUser;
    char cEnd;
} Nowcoder;
int sz = sizeof(Nowcoder);//16

 

分别 占字节数 int 4 char 1 char 1 int 4 char 1
 
对齐方式   4  1  1  ②   4   1  ③
 
圆圈的数代表补充的空字节
对齐方式: 前面的长度必须为当前要添加的字符长度的整数倍,到最后还要补齐使得最终长度是最长的字符的整数倍
 
21. break是结束整个循环体,continue是结束单次循环
此题执行if(i<1)continue; 后,结束单次循环,
后面的if(i==5)break;i++; 将不被执行,故 i 仍等于0,进入死循环
由于continue的存在,i一直不能完成自增,故导致死循环
int main()
{
    int i = 0;
    while(i < 10)
    {
        if(i < 1)
            continue;
        if(i == 5)
            break;
        i++;
    }
}

 

22.运算符重载

  • 一般情况下,单目运算符最好重载为类的成员函数;双目运算符则最好重载为类的友元函数。
  • 以下一些双目运算符不能重载为类的友元函数:=、()、[]、->。
  • 类型转换函数只能定义为一个类的成员函数而不能定义为类的友元函数。 C++提供4个类型转换函数:reinterpret_cast(在编译期间实现转换)、const_cast(在编译期间实现转换)、stactic_cast(在编译期间实现转换)、dynamic_cast(在运行期间实现转换,并可以返回转换成功与否的标志)。
  • 若一个运算符的操作需要修改对象的状态,选择重载为成员函数较好。
  • 若运算符所需的操作数(尤其是第一个操作数)希望有隐式类型转换,则只能选用友元函数。
  • 当运算符函数是一个成员函数时,最左边的操作数(或者只有最左边的操作数)必须是运算符类的一个类对象(或者是对该类对象的引用)。如果左边的操作数必须是一个不同类的对象,或者是一个内部 类型的对象,该运算符函数必须作为一个友元函数来实现。
  • 当需要重载运算符具有可交换性时,选择重载为友元函数。

 

23.基本数据类型

 

--------------------------------------------------------------------------------------------
布尔型 bool |

字符型 char,wchar_t,char16_t,char32_t |

整型 short,int,long,long long |

浮点型 float, double,long double |

无类型 void |

 

24.C++11新特性

  • nullptr替代 NULL

  • 引入了 auto 和 decltype 这两个关键字实现了类型推导

  • 基于范围的 for 循环for(auto& i : res){}

  • 类和结构体的中初始化列表

  • Lambda 表达式(匿名函数)

  • std::forward_list(单向链表)

  • 右值引用和move语义

 

25、说一说你了解的关于lambda函数的全部知识

1) 利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象;

2) 每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。

3) lambda表达式的语法定义如下:

 

4) lambda必须使用尾置返回来指定返回类型,可以忽略参数列表和返回值,但必须永远包含捕获列表和函数体;

26、简述一下堆和栈的区别?

 

posted @ 2022-03-13 23:29  huilinmumu  阅读(143)  评论(0编辑  收藏  举报