2023年伟乐视讯科技股份有限公司-C++工程师第一轮笔试题

总体评价

八股文基础偏多,整体不难,对于我这种小菜鸡略有难度(因为都还给老师了),以下答案由chatGpt+网上相关内容补充而成。

一、选择题

1.CPP类成员默认的是什么属性?

取决于访问说明符,像public,private,protected。一般你不写出来,就默认private

具体如下

class MyClass{
    
    
    int a;//默认private,私有的成员变量
public:
    void pfunc();//默认public,公有的成员函数
protected:
    void prfunc();//默认protected,受保护的成员函数
}

2.一个栈的入栈序列是A,B,C,D,E,则栈不可能出现的输出序列是?

EDCBA,DECBA,DCEAB,ABCDE

栈遵循的原则先进后出,后进先出,边进边出。

这里ChatGpt回答的不好,没给出边进边出的各种情况。

一个栈的入栈序列是 a,b,c,d,e,则栈的不可能的输出序列是( ) 。_栈不可能的输出序列_ddrrnnpp的博客-CSDN博客

3.如果派生类以protected方式继承基类,则原基类的protected和public成员在派生类的访问性分别是?

当派生类以protected方式继承基类时,原基类的protected成员在派生类中仍然保持为protected访问性,而public成员则变为protected访问性。

这意味着在派生类内部,可以直接访问基类的protected成员,而对于外部的类和对象,派生类的成员函数可以访问基类的protected成员,但其他类和对象无法直接访问。

下面是一个示例来说明这一点:

class Base {
    
    
protected:
    int protectedMember;
public:
    int publicMember;
};

class Derived : protected Base {
    
    
public:
    void foo() {
    
    
        protectedMember = 10; // 在派生类内部可以直接访问基类的protected成员
        publicMember = 20; // 在派生类内部可以直接访问基类的public成员
    }
};

int main() {
    
    
    Derived derivedObj;
    derivedObj.foo();
   
    // 在外部无法直接访问基类的protected和public成员
    // derivedObj.protectedMember; // 错误:无法访问
    // derivedObj.publicMember; // 错误:无法访问
   
    return 0;
}

在上述示例中,Derived类以protected方式继承了Base类。在Derived类的成员函数foo()中,可以直接访问基类的protectedMemberpublicMember。然而,在main()函数中,我们无法直接访问derivedObjprotectedMemberpublicMember成员。只有派生类内部和派生类的成员函数才可以直接访问这些成员。

4.CPP函数中值的传递方式有哪几种?

在C++函数中,参数的传递方式可以分为以下几种:

  1. 值传递(Pass by Value):函数接收的是实际参数的副本。在函数内部对参数的修改不会影响原始数据。

    示例:

    void func(int num) {
          
          
        num = 10; // 修改的是副本,不会改变原始数据
    }
    
    int main() {
          
          
        int value = 5;
        func(value);
        // value仍然是5
        return 0;
    }
    
  2. 引用传递(Pass by Reference):函数接收的是实际参数的引用,对参数的修改会影响原始数据。

    示例:

    void func(int& num) {
          
          
        num = 10; // 修改的是原始数据
    }
    
    int main() {
          
          
        int value = 5;
        func(value);
        // value变为10
        return 0;
    }
    
  3. 指针传递(Pass by Pointer):函数接收的是实际参数的指针,通过指针可以修改原始数据。

    示例:

    void func(int* ptr) {
          
          
        *ptr = 10; // 修改的是原始数据
    }
    
    int main() {
          
          
        int value = 5;
        func(&value);
        // value变为10
        return 0;
    }
    

这些传递方式在函数调用时选择哪种取决于需求,需要考虑参数的类型、是否需要修改原始数据等因素。

5.对于一个频繁使用的短小函数,在CPP中一般应用什么实现?

在C++中,对于频繁使用的短小函数,常见的实现方式包括以下几种:

  1. 内联函数(inline functions):使用关键字inline来定义函数。内联函数的作用是在每个调用点直接将函数的代码插入,而不是通过函数调用的方式进行执行。这样可以减少函数调用的开销,提高性能。内联函数适用于函数体较小的函数,且在多个地方频繁使用。
inline int add(int a, int b) {
    
    
    return a + b;
}
  1. 宏(macros):使用预处理器宏来定义函数。宏在编译时进行简单的文本替换,不会产生函数调用的开销。宏的定义可以包含参数,并且可以展开为复杂的表达式,但要注意宏的使用可能会引入一些副作用和难以调试的问题。
#define ADD(a, b) ((a) + (b))
  1. constexpr 函数:在 C++11 中引入的特性,用于在编译时求值的函数。constexpr 函数可以在编译时被计算出结果,从而避免了运行时的开销。constexpr 函数要求函数体和所有调用该函数的参数都必须是编译时可求值的。
constexpr int add(int a, int b) {
    
    
    return a + b;
}

这些实现方式各有优劣,选择合适的方式取决于具体的场景和需求。内联函数通常是一种比较常见和直观的选择,但是编译器是否真正内联函数的执行取决于编译器的策略和函数的复杂度。宏和 constexpr 函数可以在一些情况下提供更好的性能,但也需要注意它们的使用限制和潜在的问题。

6. 在CPP中引用与指针的区别

答案解析

正确的选项是:
A. 引用必须被初始化,指针不必要
B. 引用初始化后不能修改,指针可以
C. 不存在指向NULL的引用,但是存在指向NULL的指针

D. 引用和指针是等价的 这个选项是不正确的。

请允许我对每个选项进行解释:

A. 引用必须被初始化,指针不必要:这是正确的。在C++中,引用必须在定义时进行初始化,即引用必须引用某个对象。而指针可以在定义时不进行初始化,可以先声明指针,然后再给它赋值,也可以将指针设置为nullptr表示空指针。

B. 引用初始化后不能修改,指针可以:这也是正确的。一旦引用被初始化,它将一直引用同一个对象,无法更改引用的目标。而指针可以在任何时候改变指向,可以指向其他对象或者空值。

C. 不存在指向NULL的引用,但是存在指向NULL的指针:这也是正确的。引用必须引用有效的对象,不能引用空值。而指针可以指向空值,即nullptr,表示空指针。

D. 引用和指针是等价的:这个选项是不正确的。引用和指针是不同的概念,它们在语法和语义上有明显的区别,如上面所述。引用提供了一种方便的别名机制,而指针提供了更大的灵活性和更底层的操作能力。虽然引用可以通过指针来实现,但它们并不等价。

完全版

在C++中,引用(reference)和指针(pointer)是两种不同的概念,它们在用法和语义上有一些重要的区别。

  1. 定义和初始化:
    • 引用必须在定义时进行初始化,并且一旦初始化后,它将一直引用同一个对象,不能改变引用的目标。例如:int a = 10; int& ref = a;
    • 指针可以在定义时不进行初始化,也可以在之后指向其他对象。例如:int a = 10; int* ptr = &a;
  2. 空值(null):
    • 引用不能引用空值,必须始终引用有效的对象。
    • 指针可以指向空值,即空指针(nullptr)。
  3. 重赋值:
    • 引用一旦初始化后,就不能再引用其他对象,只能一直引用同一个对象。
    • 指针可以在任何时候改变指向,可以指向其他对象或空值。
  4. 使用:
    • 引用使用起来更加方便,可以像使用对象一样使用引用,无需解引用操作符(*)。
    • 指针需要通过解引用操作符来访问指针所指向的对象。
  5. 空间占用:
    • 引用不会占用额外的空间,它只是一个别名。
    • 指针占用的空间大小与平台相关,在32位系统中通常占用4字节,在64位系统中通常占用8字节。
  6. 空间地址:
    • 引用没有自己的地址,它与所引用的对象共享同一个地址。
    • 指针有自己的地址,它存储指向对象的地址。

总结起来,引用提供了一种方便的方式来操作对象,使代码更加简洁易读,但它有一些限制,比如不能引用空值和无法改变引用的目标。指针则更加灵活,可以在运行时改变指向,但使用时需要更加小心,因为指针可能为空或者指向无效的对象。在选择使用引用还是指针时,可以根据具体的需求和情况进行选择。

7. const符号常量:

(1) const char *p
(2) char const *p
(3) char * const p

说明上面的三种描述符的区别,正确的是()

A. (1)p是一个指向const char 的指针,p是可以改变指向的。但是p的指向的值是不能改变的;

B. (2)p指向的内容不可改变;

C. (3)p是一个指针,p指向的内容可改变,但p不可改变

D. (1)和(2)的定义是一样的

答案解析:选A

上述三种描述符的区别如下:

  1. const char *p: 这是一个指向const char类型的指针。意味着指针p所指向的字符是不可修改的,即不能通过p来修改所指向字符的值。但是指针p本身可以改变,可以指向其他字符。
  2. char const *p: 这也是一个指向const char类型的指针,与上述描述相同。const关键字位于char之前,表示char是不可修改的,与第一种描述完全等价。
  3. char * const p: 这是一个指向char类型的常量指针。意味着指针p本身是不可修改的,一旦被初始化为某个内存地址,就不能再指向其他地址。但是,通过指针p可以修改所指向字符的值,即可以改变所指向字符的内容。

总结:

  • (1)(2)是指向const char类型的指针,所指向的字符不可修改,但指针本身可以修改,注意定义不同。
  • (3)是一个常量指针,指针本身不可修改,但所指向字符的内容可以修改。

需要注意的是,这里的const关键字的位置可以在类型和指针之间交换,结果是相同的,因为它们都表示相同的含义。这是因为在C++中,const修饰符是和其左边的修饰符或类型一起解读的,而不是和右边的修饰符或类型解读的。

选项D是错误的,因为(1)(2)的定义并不相同。

尽管它们都是指向const char的指针,但是它们的语法表示稍有不同,即const关键字的位置不同。

  • (1)const位于char之前,表示指向常量的指针。
  • (2)const位于char之后,表示常量指针。

尽管它们的含义相同,都表示指针指向的字符是不可修改的,但是从语法角度来看,它们的定义是不同的。因此,选项D是错误的。

const char *p,char const *p和char *const p区别(面试题常见)_Lawrence_121的博客-CSDN博客

(1)const char *p (2)char const *p (3)char * const p 说明上面三种描述的区别。 - dartagnan - 博客园 (cnblogs.com)

const char *p 说明了 p 是指向字符串的常量指__牛客网 (nowcoder.com)

8. 如下代码,执行结果是什么?

class Base{
    
    
    public:
    Base(){
    
    printf("Base \n");};
    virtual ~Base(){
    
    printf("~Base \n");};
};

class Derived:public Base{
    
    
    public:
    Derived(){
    
    printf("Derived \n");};
    ~Derived(){
    
    printf("~Derived \n");};
};

int main(){
    
    
    Derived derived;
}

得到的结果是:

Base
Derived
~Derived
~Base

那么是怎么得到的?

这是因为代码中定义了两个类:Base和Derived。在main函数中创建了Derived类的对象derived。在创建derived对象时,会先调用Base类的构造函数,然后调用Derived类的构造函数。因此,会打印出"Base"和"Derived"。

当程序结束时,会按相反的顺序销毁对象。首先调用Derived类的析构函数,然后调用Base类的析构函数。因此,会打印出"Derived"和"Base"。

需要注意的是,在Base类的析构函数声明中存在一个拼写错误,它应该是"Base"而不是"Basse"。

二、应用题

1.某32位系统下,CPP程序,请计算sizeof的值。

char str[] = "www.wellav.com";
char *p = str;
int n = 10;
sizeof(str) = ? (1)值是多少?
sizeof(p)   = ? (2)值是多少?
sizeof(n)   = ? (3)值是多少?
void Foo(char str[100])
{
    
    
    sizeof(str) = ? (4) 值是多少?
}
void *p = malloc(100);
sizeof(p) = ? (5)值是多少?

根据给出的代码和说明,可以计算出以下的sizeof值:

(1) sizeof(str)的值是 15。str是一个字符数组,它包含的字符数为14个,再加上结尾的空字符\0,共占用15个字节的内存空间。

(2) sizeof(p)的值是 4。p是一个指针,无论指向的是什么类型的数据,指针本身的大小是固定的。在32位系统中,指针的大小为4个字节。

(3) sizeof(n)的值是 4。n是一个int类型的变量,在32位系统中,int类型占用4个字节的内存空间。

(4) sizeof(str)的值是 4。在函数中,str是一个指针,传递给函数的数组参数会自动转换为指针。因此,sizeof(str)返回的是指针的大小,而不是数组的大小。在32位系统中,指针的大小为4个字节。

(5) sizeof(p)的值是 4。p是一个void*类型的指针,无论指向的是什么类型的数据,指针本身的大小是固定的。在32位系统中,指针的大小为4个字节。

2. 以下代码是否正确?如果错误,请说明原因

typedef vector<int> IntArray;
IntArray array;
array.push_back(1);
array.push_back(2);
array.push_back(2);
array.push_back(4);
//删除array数组中所有的2
for(IntArray::iteratot itor=array.begin();itor!=array.end();++itor)
{
    
    
    if(2==*itor) itor=array.erase(itor);
}

答案解析

错误原因:

  1. 在for循环中,迭代器itor在循环内部可能会被修改。当执行itor=array.erase(itor)时,如果删除了当前元素,itor会指向已删除元素的下一个位置,然后在循环的自增部分会再次对itor进行自增操作,导致迭代器无效。

解决方法:

可以使用std::remove算法结合vectorerase方法来实现删除指定元素的操作。修正后的代码如下:

typedef vector<int> IntArray;
IntArray array;
array.push_back(1);
array.push_back(2);
array.push_back(2);
array.push_back(4);
//删除array数组中所有的2
array.erase(std::remove(array.begin(), array.end(), 2), array.end());

这样会先使用std::remove算法将所有的2移动到数组的末尾,然后再使用vectorerase方法擦除从移动后的2开始的部分,达到删除所有2的效果。

3.在什么情况下使用纯虚函数(pure virtual function)?

纯虚函数(pure virtual function)在以下情况下使用:

  1. 定义一个抽象类(Abstract Class):抽象类是一种不能被实例化的类,它主要用于作为其他类的基类。抽象类中包含纯虚函数,这些函数没有实际的实现,需要在派生类中进行实现。通过将类中至少一个函数声明为纯虚函数,可以使该类成为抽象类。派生类必须实现纯虚函数才能被实例化。

举例说明:

class Shape {
    
    
public:
    virtual void draw() = 0; // 纯虚函数
    virtual void calculateArea() = 0; // 纯虚函数
};

class Circle : public Shape {
    
    
public:
    void draw() override {
    
    
        // 实现绘制圆形的代码
    }

    void calculateArea() override {
    
    
        // 实现计算圆形面积的代码
    }
};

class Square : public Shape {
    
    
public:
    void draw() override {
    
    
        // 实现绘制正方形的代码
    }

    void calculateArea() override {
    
    
        // 实现计算正方形面积的代码
    }
};

int main() {
    
    
    // 不能实例化Shape对象,但可以通过指针或引用调用接口函数
    Shape* shapePtr = new Circle();
    shapePtr->draw();
    // Circle和Square类是具体派生类,可以被实例化
    Circle circle;
    Square square;
    circle.draw();
    square.calculateArea();
    return 0;
}

在上述例子中,Shape是一个抽象类,其中的纯虚函数draw()calculateArea()没有具体的实现。派生类CircleSquare必须实现这两个纯虚函数才能被实例化。

  1. 接口类(Interface Class):纯虚函数用于定义接口类,接口类主要用于定义一组接口规范,派生类必须实现这些接口。接口类中的函数都是纯虚函数,没有具体的实现。

举例说明:

class Printable {
    
    
public:
    virtual void print() const = 0; // 纯虚函数
};

class Book : public Printable {
    
    
public:
    void print() const override {
    
    
        // 打印图书信息的具体实现
    }
};

class Magazine : public Printable {
    
    
public:
    void print() const override {
    
    
        // 打印杂志信息的具体实现
    }
};

int main() {
    
    
    Book book;
    Magazine magazine;
    book.print();
    magazine.print();
    return 0;
}

在上述例子中,Printable是一个接口类,它定义了一个纯虚函数print(),没有具体的实现。派生类BookMagazine必须实现print()函数以满足接口规范。这样可以实现不同类型的打印操作,而具体的实现由各个派生类完成。

4.下面函数有什么问题

int &pe(int r,int i)
{
    
    
    Int re = r*i;
    return re
}

答案解析

下面的函数存在以下问题:

  1. 函数返回了对局部变量的引用:在函数内部定义的变量re是一个局部变量,它在函数执行完毕后会被销毁。然而,该函数却返回了对re的引用,这会导致返回的引用指向一个无效的内存地址。访问该引用将导致未定义行为。
  2. 函数声明中的类型错误:函数声明中的Int应该是int的拼写错误。

修正后的函数如下所示:

int pe(int r, int i)
{
    
    
   int re = r * i;
   return re;
}

在修正后的函数中,将局部变量re的类型更正为int,并将其作为普通的返回值返回。这样可以确保返回的是一个有效的值,而不是对局部变量的引用。

5. 用什么方法防止类对象被拷贝和赋值?

要防止类对象被拷贝和赋值,可以采用以下两种方法:

  1. 禁用拷贝构造函数和赋值操作符:可以将类的拷贝构造函数和赋值操作符声明为私有,并且不提供实现。这样一来,类的对象就无法进行拷贝和赋值操作。
class MyClass {
    
    
private:
    MyClass(const MyClass&); // 禁用拷贝构造函数
    MyClass& operator=(const MyClass&); // 禁用赋值操作符

public:
    // 其他成员和函数的定义
};

在上述代码中,通过将拷贝构造函数和赋值操作符声明为私有,外部代码无法访问这些函数,从而阻止了对象的拷贝和赋值。

  1. 使用C++11的delete关键字:可以使用C++11引入的delete关键字显式删除拷贝构造函数和赋值操作符的默认实现。
class MyClass {
    
    
public:
    MyClass(const MyClass&) = delete; // 显式删除拷贝构造函数
    MyClass& operator=(const MyClass&) = delete; // 显式删除赋值操作符

    // 其他成员和函数的定义
};

在上述代码中,通过使用= delete显式删除拷贝构造函数和赋值操作符的默认实现,可以防止对象的拷贝和赋值。

这两种方法都可以有效地防止类对象的拷贝和赋值,使得类的对象在使用时只能通过特定的方式进行创建和赋值,从而更好地控制对象的生命周期和状态。

6. 头文件中使用#ifndef/#define/#endif的作用是什么?

答案解析

在C++中,使用#ifndef、#define和#endif组合的作用是防止头文件的重复包含,即防止同一个头文件被多次包含在同一个源文件中。

具体作用如下:

  1. #ifndef(如果未定义):该指令用于检查一个宏是否已经在当前代码中被定义。如果指定的宏已经定义,则跳过后续的代码块,进入到#endif处。如果指定的宏未定义,则继续执行后续的代码块。
  2. #define(定义):该指令用于定义一个宏。在头文件中,通常使用一个特定的宏名来标识该头文件,一般是使用大写字母和下划线的组合。该宏的值可以为空,也可以为一个非空值。
  3. #endif(结束):该指令表示条件编译的结束。它和#ifndef配对使用,标志着条件编译的范围的结束。

通过使用#ifndef、#define和#endif,可以防止头文件的多重包含。在首次包含头文件时,由于宏未定义,条件编译会通过,并定义该宏。当再次遇到相同的头文件时,由于宏已经定义,条件编译会跳过头文件的内容,从而避免了重复包含的问题。

例如,以下是一个示例:

#ifndef MY_HEADER_H
#define MY_HEADER_H

// 头文件的内容

#endif // MY_HEADER_H

在上述示例中,MY_HEADER_H是一个自定义的宏名称,用于标识该头文件。如果在同一个源文件中多次包含该头文件,只有在首次包含时,条件编译会通过,并定义该宏。后续的包含操作会因为宏已经定义而跳过头文件的内容,从而避免了重复包含的问题。

什么是头文件重复保护

头文件重复包含是指在同一个源文件中多次包含同一个头文件的情况。当一个头文件被多次包含时,会导致一些问题和错误。

问题和错误包括:

  1. 定义重复:如果头文件中包含了类型定义、全局变量、宏定义等,多次包含同一个头文件会导致这些定义重复出现,从而引发重定义错误。
  2. 函数重复定义:如果头文件中包含了函数的定义,多次包含同一个头文件会导致这些函数的多重定义,从而引发重定义错误。
  3. 依赖关系错误:如果多个头文件相互包含,而这些头文件又包含同一个头文件,就会形成循环包含的情况,导致编译错误。

头文件重复包含的问题可以通过使用条件编译指令来解决,如使用#ifndef、#define和#endif组合。这样可以防止同一个头文件被多次包含在同一个源文件中,确保头文件的定义和声明只会出现一次,避免重复定义和编译错误的发生。

7.请说明CPP中namespace关键字的应用场景

在C++中,namespace关键字用于创建命名空间(namespace),用于将全局作用域分隔为不同的命名空间,以解决命名冲突和组织代码的问题。namespace关键字的应用场景如下:

  1. 命名冲突的解决:当在一个程序中使用多个第三方库或多个模块时,不同的库或模块可能会定义相同的名称。这样就可能导致命名冲突,编译器无法区分使用的是哪个定义。通过将不同的代码放置在不同的命名空间中,可以避免命名冲突的问题。每个命名空间中的名称不会与其他命名空间中的名称发生冲突。
  2. 代码组织和模块化:命名空间可以用于将相关的代码组织在一起,形成一个逻辑上的模块或子系统。通过使用命名空间,可以将相关的类、函数、变量等放置在同一个命名空间中,使代码更加结构化和模块化,提高代码的可读性和可维护性。
  3. 第三方库和框架的封装:在开发使用第三方库或框架的程序时,可以将库或框架的代码放置在一个独立的命名空间中,以避免与自己的代码发生命名冲突。这样可以更好地封装第三方库的功能,并且在使用时明确指定命名空间,提高代码的可读性和可维护性。
  4. 嵌套命名空间:命名空间可以进行嵌套,即在一个命名空间中定义另一个命名空间。这样可以进一步组织和划分代码的结构,形成更细粒度的命名空间。

示例:

namespace Math {
    
    
    // Math命名空间中的函数和变量
    int add(int a, int b);
    int subtract(int a, int b);

    namespace Geometry {
    
    
        // Geometry命名空间中的函数和变量
        double calculateArea(double radius);
        double calculatePerimeter(double side);
    }
}

int main() {
    
    
    int result = Math::add(3, 4);
    double area = Math::Geometry::calculateArea(2.5);
    return 0;
}

在上述示例中,Math命名空间中定义了两个函数add()和subtract(),以及嵌套的Geometry命名空间,其中定义了calculateArea()和calculatePerimeter()函数。通过使用命名空间,可以明确指定使用的函数或变量所属的命名空间,避免了命名冲突,并且能够更好地组织和管理代码。

8.请分别说出这些类成员变了的名称是否合理,为什么?

string valueA; //姓名
int iValueB; //年龄
double m_iValueC; //分数

开放题

对于给定的类成员变量名称:

  1. string valueA; - 姓名

这个成员变量的名称valueA代表姓名,是合理的。变量名应该能够清晰地描述其所代表的含义,使用有意义的名称可以增加代码的可读性和可理解性。

  1. int iValueB; - 年龄

这个成员变量的名称iValueB代表年龄,也是合理的。虽然名称中包含了类型信息(i表示整数),但这是一个常见的命名约定。在命名时,可以考虑使用更具描述性的名称,例如age,以进一步提高代码的可读性。

  1. double m_iValueC; - 分数

这个成员变量的名称m_iValueC代表分数,也是合理的。m前缀可能表示成员变量(member),而i可能表示整数类型,虽然在这里是一个double类型的变量。类成员变量的命名约定可以因项目、团队或个人而异,但应该选择具有描述性的名称,以便更清晰地表达其含义(老委婉了)。因此,对于这个变量,使用更具描述性的名称,例如score,可能会更好。

总结:尽量使用具有描述性的名称来命名变量,以使代码更易于理解和维护。选择合适的变量名可以提高代码的可读性,并帮助其他开发人员快速理解变量的含义和用途。

扩展:命名法

驼峰命名法(Camel Case)是一种常见的命名风格,其中单词之间没有分隔符,而是通过将每个单词的首字母大写来区分单词。以下是驼峰命名法的几种常见形式:

  1. 小驼峰命名法(Lower Camel Case):第一个单词的首字母小写,后续每个单词的首字母大写。例如:myVariableName, firstName, totalCount
  2. 大驼峰命名法(Upper Camel Case):每个单词的首字母都大写。通常用于类名或类型名的命名。例如:Person, MyClass, PhoneNumber

驼峰命名法在很多编程语言中被广泛使用,包括Java、C++、C#、JavaScript等。

除了驼峰命名法,常见的命名风格还包括以下几种:

  1. 下划线命名法(Snake Case):将单词用下划线连接,每个单词都使用小写字母。例如:value_a, i_value_b, m_i_value_c。下划线命名法在很多编程语言中被广泛使用,包括C、Python等。
  2. 全大写命名法(UPPER CASE):将单词全部大写,单词之间使用下划线分隔。通常用于常量的命名。例如:VALUE_A, I_VALUE_B, M_I_VALUE_C
  3. 小写命名法(Lower Case):所有字母都使用小写,单词之间没有分隔符。例如:valuea, ivalueb, mivaluec。这种命名风格在某些编程语言中被广泛使用,如Lisp和Scheme。
  4. 匈牙利命名法(Hungarian Notation):在变量名前加上一个或多个表示类型的前缀,以及后续的驼峰命名法或下划线命名法。例如:strValueAnIValueBdM_IValueC。匈牙利命名法在过去较为流行,但在现代的编程实践中已不太常见。

这些命名风格的选择取决于编程语言、项目约定和个人偏好。重要的是保持一致性,在同一个项目或团队中遵循相同的命名风格,以提高代码的可读性和可维护性。

9.如下代码输出什么?

class A
{
    
    
public:
    virtual void fun1() {
    
     printf("in A fun1\n"); };
    void fun2() {
    
     printf("in A fun2\n"); };
};

class B :public A {
    
    
public:
    virtual void fun1() {
    
     printf("in B fun1\n"); };
    void fun2() {
    
     printf("in B fun2\n"); };
};

int main() {
    
    
    B obj;
    A* p = new B();
    obj.fun1();
    obj.fun2();
    p->fun1();
    p->fun2();
}

得到的结果如下

in B fun1
in B fun2
in B fun1
in A fun2

到底是为什么?

答案解析

这是因为类A和类B之间存在继承关系。类A中的函数fun1()和fun2()被声明为虚函数,而类B通过继承类A来重写这些虚函数。

在代码中,对象obj是类B的一个实例,所以调用obj.fun1()会调用类B中重写的fun1()函数,输出"in B fun1"。同样,调用obj.fun2()会调用类B中的fun2()函数,输出"in B fun2"。

指针p被声明为指向类A的指针,并通过new操作符创建了一个类B的对象,并将其赋值给指针p。当使用指针p调用p->fun1()时,由于fun1()在类A中被声明为虚函数,并在类B中进行了重写,所以会调用类B中的fun1()函数,输出"in B fun1"。然而,当使用指针p调用p->fun2()时,由于fun2()在类A中没有被声明为虚函数,所以会根据指针的类型(类A)来调用对应的函数,输出"in A fun2"

总结起来,虚函数的调用取决于指针或引用所指向的对象的实际类型,而非指针或引用本身的类型。而非虚函数的调用则取决于指针或引用的类型。

三、综合题

1. OSI参考模型有哪7层?TCP/IP有哪4层?FTP、TCP分别是属于哪一层的协议?

回答问题1:OSI(Open Systems Interconnection)参考模型包含以下七层:

  1. 物理层(Physical Layer):负责传输比特流,并处理与物理介质的接口相关的电气、力学和功能特性。

  2. 数据链路层(Data Link Layer):提供可靠的点对点数据传输,通过物理寻址、差错检测和流量控制等机制,将原始比特流转换为有意义的帧。

  3. 网络层(Network Layer):负责数据包的路由选择和转发,实现不同网络之间的通信,包括网络寻址、逻辑编址和路由选择等功能。

  4. 传输层(Transport Layer):提供端到端的可靠数据传输,确保数据的完整性、可靠性和流量控制,例如分段、重组、确认和重传等。

  5. 会话层(Session Layer):负责建立、管理和终止会话(会话是两个应用程序之间的通信会话),并提供会话层的控制和同步。

  6. 表示层(Presentation Layer):处理数据的表示和转换,确保不同系统的数据格式能够相互理解,提供数据的加密、解密、压缩和格式转换等功能。

  7. 应用层(Application Layer):提供特定应用程序的服务和协议,例如电子邮件、文件传输和远程登录等。

回答问题2:TCP/IP协议族采用的是一种更简化的四层模型,包括以下层:

  1. 网络接口层(Network Interface Layer):与物理网络介质直接交互,负责数据链路层和物理层的功能。

  2. 网络层(Internet Layer):类似于OSI模型的网络层,负责网络寻址、路由选择和数据包的转发。

  3. 传输层(Transport Layer):与OSI模型的传输层功能相同,提供端到端的可靠数据传输,主要使用TCP和UDP协议。

  4. 应用层(Application Layer):类似于OSI模型的应用层,提供各种应用程序的服务和协议,如HTTP、FTP、SMTP等。

回答问题3

FTP(File Transfer Protocol)是属于应用层的协议,用于在客户端和服务器之间进行文件传输。TCP(Transmission Control Protocol)是属于传输层的协议,提供可靠的端到端数据传输。

2.linux进程间通信有哪些方式?

在Linux中,进程间通信(IPC,Inter-Process Communication)有多种方式,常用的包括以下几种:

  1. 管道(Pipe):管道是一种半双工的通信方式,可以在父进程与子进程之间传递数据。它分为无名管道(Anonymous Pipe)和命名管道(Named Pipe)。无名管道只能在具有亲缘关系的进程之间使用,而命名管道可以被无关的进程使用。
  2. 命名管道(FIFO):命名管道也称为FIFO(First-In, First-Out),它提供了一种进程间有名字的通信方式,允许无关的进程之间进行通信。
  3. 共享内存(Shared Memory):共享内存是一种高效的进程间通信方式,允许多个进程访问同一块物理内存区域,进程可以直接读写该内存区域,避免了数据的复制。
  4. 信号量(Semaphore):信号量是一种用于进程同步和互斥的机制,它通过一个计数器来控制对共享资源的访问。进程可以对信号量进行操作,如等待信号量、释放信号量等。
  5. 消息队列(Message Queue):消息队列是一种可以在进程之间传递消息的机制,消息被放置在一个队列中,其他进程可以从队列中读取消息。
  6. 套接字(Socket):套接字是一种用于不同主机之间进行进程间通信的机制。在本地进程间通信时,也可以使用Unix域套接字(Unix Domain Socket)。
  7. 信号(Signal):信号是一种异步通信机制,用于通知进程发生了某个事件。进程可以注册信号处理函数来捕获并处理信号。

这些方式各有特点,适用于不同的场景和需求。开发者可以根据具体情况选择合适的进程间通信方式。

3. MySQL操作,表T_USER(id,name,info),编写SQL语句实现下述要求。

(1)如何向表中插入一条数据?

(2)查找id=100的用户?

(3)删除id=20的用户?

(4)修改id=10的用户的名字为123456?

答案解析

以下是对应要求的SQL语句实现:

(1) 向表中插入一条数据:

INSERT INTO T_USER (id, name, info) VALUES (1, 'John', 'Some information');

(2) 查找id=100的用户:

SELECT * FROM T_USER WHERE id = 100;

(3) 删除id=20的用户:

DELETE FROM T_USER WHERE id = 20;

(4) 修改id=10的用户的名字为123456:

UPDATE T_USER SET name = '123456' WHERE id = 10;

请注意,以上示例中的表名、字段名以及具体的值根据实际情况进行替换。

4.如下查找方式最快是哪个?为什么?

二分查找,遍历查找,hash查找,随机查找

答案解析

在一般情况下,二分查找是最快的查找方式,原因如下:

  1. 时间复杂度:二分查找的时间复杂度是O(log n),其中n是查找范围内元素的个数。相比之下,遍历查找、随机查找和哈希查找的时间复杂度通常为O(n)。
  2. 数据有序性要求:二分查找要求数据是有序的,但一旦数据有序,二分查找的效率非常高。通过每次将查找范围减半,二分查找能够快速地定位目标元素所在的位置。
  3. 适用范围:遍历查找、随机查找和哈希查找通常适用于无序数据的查找。在这些情况下,需要遍历整个数据集或通过哈希函数计算索引位置,无法像二分查找那样快速缩小查找范围。

需要注意的是,以上结论是在一般情况下成立的。具体的查找方式最快与问题的特定情况相关。例如,如果数据量较小且有序性不高,遍历查找可能比二分查找更快速。因此,在选择最快的查找方式时,需要综合考虑数据的特点、有序性要求以及实际的性能需求。

补充:hash查找和随机查找为什么不是最优的?

  1. Hash查找的限制:Hash查找依赖于哈希函数将键映射到存储位置,因此在设计哈希函数时需要考虑均匀分布和避免冲突。如果哈希函数设计不合理或者存在大量的哈希冲突,会导致查找效率下降。此外,如果需要进行范围查找或模糊匹配,Hash查找无法满足这些需求。
  2. 随机查找的低效性:随机查找通常是通过在整个数据集中随机选择元素进行比较来实现。这种查找方式的效率较低,因为在平均情况下,需要遍历大部分数据才能找到目标元素。随机查找适用于小规模数据集,但对于大规模数据集来说,效率较低。
  3. 不适用于有序数据:Hash查找和随机查找不适用于有序数据集。在有序数据集中,二分查找能够利用数据的有序性,在每一次比较中能够快速缩小查找范围,因此效率更高。而Hash查找和随机查找没有利用到数据的有序性,无法充分发挥优势。

5. 编写C/CPP代码,写一个函数,把字符串转成整形。

下面是一个简单的C++函数示例,将字符串转换为整数:

#include <iostream>
#include <string>

int stringToInt(const std::string& str) {
    
    
    int result = 0;
    int sign = 1; // 符号位,默认为正数
    int i = 0;

    // 跳过字符串前面的空格
    while (str[i] == ' ') {
    
    
        i++;
    }

    // 处理正负号
    if (str[i] == '-' || str[i] == '+') {
    
    
        sign = (str[i++] == '-') ? -1 : 1;
    }

    // 转换数字部分
    while (str[i] >= '0' && str[i] <= '9') {
    
    
        // 判断是否溢出
        if (result > INT_MAX / 10 || (result == INT_MAX / 10 && (str[i] - '0') > INT_MAX % 10)) {
    
    
            return (sign == 1) ? INT_MAX : INT_MIN;
        }
        result = result * 10 + (str[i++] - '0');
    }

    return result * sign;
}

int main() {
    
    
    std::string str = "12345";
    int result = stringToInt(str);
    std::cout << "Result: " << result << std::endl;

    return 0;
}

该函数通过遍历字符串的字符,并根据数字的规则进行转换,包括处理符号位、空格、溢出等情况。最后返回转换后的整数值。

需要注意的是,以上示例仅为简单实现,未考虑所有的输入情况和错误处理。在实际开发中,可能需要进一步完善和优化函数,以应对更多的输入情况和错误处理。

6.编写C/CPP代码,实现一个把字符串倒序的功能,如“abcd”倒序后变为”dcba“。

下面是一个简单的C++函数示例,实现将字符串倒序的功能:

#include <iostream>
#include <string>

std::string reverseString(const std::string& str) {
    
    
    std::string reversedStr;
    int length = str.length();

    // 从字符串末尾开始遍历,逐个字符添加到新字符串中
    for (int i = length - 1; i >= 0; i--) {
    
    
        reversedStr += str[i];
    }

    return reversedStr;
}

int main() {
    
    
    std::string str = "abcd";
    std::string reversedStr = reverseString(str);
    std::cout << "Reversed String: " << reversedStr << std::endl;

    return 0;
}

该函数通过从字符串末尾开始遍历,逐个字符地将其添加到一个新的字符串中,从而实现字符串的倒序。在循环中,初始索引为字符串长度减一,逐步递减直至索引为零,将字符按相反的顺序添加到新字符串中。

需要注意的是,以上示例仅为简单实现,未考虑输入为空字符串的情况。在实际开发中,可能需要进一步完善和优化函数,以应对更多的输入情况和错误处理。

猜你喜欢

转载自blog.csdn.net/kokool/article/details/130855117