Python && C++ 面经

面向过程和面向对象

answer::

C/C++中面向对象的相关知识

面向对象程序设计(Object-oriented programming,OOP)有三大特征 ——封装、继承、多态

封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
关键字:public, protected, private。不写默认为 private。
1.public 成员:可以被任意实体访问。
2.protected 成员:只允许被子类及本类的成员函数访问。
3.private 成员:只允许被本类的成员函数、友元类或友元函数访问。

继承:基类(父类)——> 派生类(子类)

多态:即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。多态是以封装和继承为基础的。
C++ 多态分类及实现:
1.重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
2.子类型多态(Subtype Polymorphism,运行期):虚函数
3.参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
4.强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换

C++类和对象

一.什么是面向对象的程序设计

1. 对象

  • 客观世界中任何一个事物都可以看成一个对象。
  • 对象是构成系统的基本单位。
  • 任何一个对象都具有静态和动态的特征。
  • 静态特征称为属性,动态特征称为行为,外界给对象发出的信息一般称作消息。
  • 一个对象往往包含一组属性和一组行为。
  • 在C++中,每个对象由数据和函数(操作代码)两部分组成

2. 封装与信息隐蔽

  • 对一个对象进行封装处理,就是把它的一部分属性和功能向外界屏蔽。也就是说把对象的内部实现和外部行为分隔开来。
  • 封装性是面向对象程序设计的一个重要特点,封装在此具有两个含义:

   1.把有关的数据和函数封装在一个对象里,形成程序中的一个基本单位,各个对象之间互不干扰。

   2.把对象中的某些部分对外屏蔽,只留下与外界联系的接口接收外界消息称之为信息屏蔽

3. 抽象

  • 将一组同类对象的共同特征抽象出来,从而形成类的概念。类是对象的抽象,而对象是类的具体实例。

4. 继承与重用

  • 简单来说就是在之前建立的类的基础上增加新函数功能,从而变成一个新的类。

5. 多态性

  • 在C++中的多态性是指,由继承产生的新类,它的对象对同一个消息会作出不同的响应。例子就是一个可执行文本点击执行就是执行文件中的程序,如果这个可执行文本改成文本文件,则会启动一个编辑器来打开它。

二.面向对象程序设计的特点

面向对象程序设计包括两个方面:
  设计所需的各种类和对象,即决定把哪些数据和操作封装在一起。
  考虑怎样调用对象的成员函数实现所需操作。

三.类和对象的作用

  • 是C++的灵魂,它是实现面向对象程序设计的基础
  • C++支持面向过程的程序设计,也支持基于对象和面向对象的程序设计。
  • 基于对象就是基于类,基于对象的程序就是以类和对象为基础的,程序的操作是围绕对象进行的,再利用继承机制和多态性就成为了面向对象的程序设计。
  • 把一组数据和函数放在一起,这就是面向对象程序设计中的对象。
  • 程序=数据结构+算法
  • 面向过程的程序设计是以数据结构为基础的算法设计。面向对象程序设计就是把一个算法和一组数据结构封装在一个对象中
  • 对象=数据结构+算法
  • 程序=(对象+对象+…+对象)+消息,消息的作用就是对对象进行控制,以及确定向对象发出的命令,让对象完成相应的任务。

类的声明和对象的定义

1.类和对象的关系

  • 类(class)是对象的抽象,不占用内存空间。对象是类的具体事例,占用内存空间。类是对象的类型。

2.声明类类型

  • 类是要用户自己定义的类型,声明一个类类型与声明一个结构体类型相似。
    在这里插入图片描述
    在这里插入图片描述

3.定义对象的方法

先声明类类型,后定义对象
在这里插入图片描述

在声明类类型的同时定义对象
在这里插入图片描述
不出现类名,直接定义对象(合法但不提倡使用)
在这里插入图片描述
类和结构体类型的异同
两种定义方法还是有区别的:
用class声明的类如果不带成员访问限定符,所有成员默认限定为private
用struct声明的类如果不带成员访问限定符,所有成员默认限定为public

类的成员函数

  • 访问类数据的成员函数叫做类的成员函数。

成员函数的性质

  • 成员函数可以访问本类中的所有成员,由于成员函数的成员访问限定符是public,可以通过它来访问类的其他成员,因此指定为public的成员函数可以作为类的外界的接口。

在类外定义成员函数

  • C++允许在类内声明成员函数的原型,然后在类外定义成员函数。 :: 是 作用域限定符 或称 作用域运算符 ,如果不使用这个符号函数就成了全局作用域中的display函数而不是成员函数。如果在 :: 前不带类名或函数名前既无类名也无 :: ,则表示函数是全局函数。类外定义函数的格式:
    在这里插入图片描述
    在这里插入图片描述

inline成员函数(内联函数)

  • C++默认在类内定义的成员函数是inline函数不用加上inline关键字,在域外定义inline函数需在函数声明时加上inline关键字。

在这里插入图片描述

成员函数的存储方式

  • C++只为对象的数据成员分配内存空间,一个类的所有对象共享一个成员函数空间,解决了系统为每个对象的成员函数都分配内存空间而造成大量浪费的问题。

对象成员的引用

一.用对象名和成员运算符访问对象中的成员

在这里插入图片描述

  • 注意只有成员函数才可以访问类中所有成员,而在类外只能访问共有成员,且不能进行赋值等操作。

通过指向对象的指针访问对象中的成员

在这里插入图片描述

通过对象的引用来访问对象中的成员

  • 就是基本的C++中引用和指针的概念
    在这里插入图片描述

C/C++中智能指针的定义与作用

智能指针是一个类,这个类的构造函数中传入一个普通指针,析构函数中释放传入的指针。智能指针的类都是栈上的对象,所以当函数(或程序)结束时会自动被释放。

(注:不能将指针直接赋值给一个智能指针,一个是类,一个是指针。)

常用的智能指针:智能指针在C++11版本之后提供,包含在头文件中,主要是shared_ptr、unique_ptr、weak_ptr。unique_ptr不支持复制和赋值。当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果原来的unique_ptr 将存在一段时间,编译器将禁止这么做。shared_ptr是基于引用计数的智能指针。可随意赋值,直到内存的引用计数为0的时候这个内存会被释放。weak_ptr能进行弱引用。引用计数有一个问题就是互相引用形成环,这样两个指针指向的内存都无法释放。需要手动打破循环引用或使用weak_ptr。顾名思义,weak_ptr是一个弱引用,只引用,不计数。如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。所以weak_ptr不保证它指向的内存一定是有效的,在使用之前需要检查weak_ptr是否为空指针。

智能指针的作用:C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,野指针,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

C/C++中数组和链表的优缺点

数组和链表是C/C++中两种基本的数据结构,也是两个最常用的数据结构。

数组的特点是在内存中,数组是一块连续的区域,并且数组需要预留空间。链表的特点是在内存中,元素的空间可以在任意地方,空间是分散的,不需要连续。链表中的元素都会两个属性,一个是元素的值,另一个是指针,此指针标记了下一个元素的地址。每一个数据都会保存下一个数据的内存的地址,通过此地址可以找到下一个数据。

数组的优缺点

优点:查询效率高,时间复杂度可以达到O(1)。

缺点:新增和修改效率低,时间复杂度为O(N);内存分配是连续的内存,扩容需要重新分配内存。

链表的优缺点

优点:新增和修改效率高,只需要修改指针指向即可,时间复杂度可以达到O(1);内存分配不需要连续的内存,占用连续内存少。

缺点:链表查询效率低,需要从链表头依次查找,时间复杂度为O(N)。

C/C++中的new和malloc有什么区别

new和malloc主要有以下三方面的区别:

  malloc和free是标准库函数,支持覆盖;new和delete是运算符,支持重载。

  malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。

  malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。

C/C++中宏定义的相关知识

宏定义可以把一个名称指定成任何一个文本。在完成宏定义后,无论宏名称出现在源代码的何处,预处理器都会将其替换成指定的文本。

//define 宏名 文本
#define LSH 666688889999

//define 宏名(参数) 文本
#define R(a,b) (a/b)
//注:带参数的宏替换最好在表达式整体上加括号,避免结果受其他运算影响。

宏定义的优点:

  1. 方便程序修改,如果一个常量在程序中大量使用,我们可以使用宏定义为其设置一个标识符。当我们想修改这个常量时,直接修改宏定义处即可,不必在程序中海量寻找所有相关位置。
  2. 提高程序的运行效率,使用带参数的宏定义可以完成函数的功能,但同时又比函数节省系统开销,提升程序运行效率。(无需调用函数这个流程)

宏定义和函数的区别:

  1. 宏在预处理阶段完成替换,之后替换的文本参与编译,相当于是恒等代换过程,运行时不存在函数调用,执行起来更快;而函数调用在运行时需要跳转到具体调用函数。
  2. 宏定义没有返回值;函数调用具有返回值。
  3. 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
  4. 宏定义不是说明或者语句,结尾不用加分号。
  5. 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用# undef命令;而函数作用域在函数调用处。

C/C++中typedef关键字的相关知识

我们可以使用typedef关键字来定义自己习惯的数据类型名称,来替代系统默认的基本类型名称以及其他类型等名称。

在工业界中,我们一般在如下两个场景中会见到typedef的身影。

// 1.为基本数据类型定义新的类型名
typedef unsigned int LSH_int;
typedef char* LSH_point;
  
// 2.为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称
typedef struct target_Object
{
    
    
    int x;
    int y;
} LSH_Object;

typedef与宏定义的区别

  1. 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
  2. 宏替换发生在预处理阶段,属于文本恒等替换;typedef是编译中发挥作用。
  3. 宏定义参数没有类型,不进行类型检查;typedef参数具有类型,需要检查类型。
  4. 宏不是语句,不用在最后加分号;typedef是语句,要加分号标识结束。
  5. 注意对指针的操作,typedef char * p_char和#define p_char char *区别巨大。

TCP/IP四层模型的相关概念

TCP/IP四层模型:

1.应用层:负责各种不同应用之间的协议,如文件传输协议(FTP),远程登陆协议(Telnet),电子邮件协议(SMTP),网络文件服务协议(NFS),网络管理协议(SNMP)等。

2.传输层:负责可靠传输的TCP协议、高效传输的UDP协议。

3.网络层:负责寻址(准确找到对方设备)的IP,ICMP,ARP,RARP等协议。

4.数据链路层:负责将数字信号在物理通道(网线)中准确传输。

四层模型逻辑:
发送端是由上至下,把上层来的数据在头部加上各层协议的数据(部首)再下发给下层。

接受端则由下而上,把从下层接收到的数据进行解密和去掉头部的部首后再发送给上层。

层层加密和解密后,应用层最终拿到了需要的数据。

OSI七层模型的相关概念
在这里插入图片描述

C++ 中 vector 和 list 的区别:

vector
连续存储的容器,动态数组,在堆上分配空间

底层实现:数组

两倍容量增长:

vector 增加(插入)新元素时,如果未超过当时的容量,则还有剩余空间,那么直接添加到最后(插入指定位置),然后调整迭代器。

如果没有剩余空间了,则会重新配置原有元素个数的两倍空间,然后将原空间元素通过复制的方式初始化新空间,再向新空间增加元素,最后析构并释放原空间,之前的迭代器会失效。

性能:

访问:O(1)

插入:在最后插入(空间够):很快

在最后插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。

在中间插入(空间够):内存拷贝

在中间插入(空间不够):需要内存申请和释放,以及对之前数据进行拷贝。

删除:在最后删除:很快

在中间删除:内存拷贝

适用场景:经常随机访问,且不经常对非尾节点进行插入删除。

List
动态链表,在堆上分配空间,每插入一个元数都会分配空间,每删除一个元素都会释放空间。

底层:双向链表

性能:

访问:随机访问性能很差,只能快速访问头尾节点。

插入:很快,一般是常数开销

删除:很快,一般是常数开销

适用场景:经常插入删除大量数据,但是访问起来没有数组方便。

区别
1)vector底层实现是数组;list是双向 链表。
2)vector支持随机访问,list不支持。
3)vector是顺序内存,list不是。
4)vector在中间节点进行插入删除会导致内存拷贝,list不会。
5)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
6)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。

应用
vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随即访问,而不在乎插入和删除的效率,使用vector。
list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,则应使用list。

   
 
 
 

Python相关面经

Python中生成器的相关知识

我们创建列表的时候,受到内存限制,容量肯定是有限的,而且不可能全部给他一次枚举出来。Python常用的列表生成式有一个致命的缺点就是定义即生成,非常的浪费空间和效率。

如果列表元素可以按照某种算法推算出来,那我们可以在循环的过程中不断推算出后续的元素,这样就不必创建完整的list,从而节省大量的空间。在Python中,这种一边循环一边计算的机制,称为生成器:generator

要创建一个generator,最简单的方法是改造列表生成式:

a = [x * x for x in range(10)]
print(a)
b = (x * x for x in range(10))
print(b)
print(list(b))

for i in b:
    print(i)

--------结果如下--------------
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
<generator object <genexpr> at 0x10557da50>
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
0
1
4
9
16
25
36
49
64
81

还有一个方法是生成器函数,通过def定义,然后使用yield来支持迭代器协议,比迭代器写起来更简单。

def spam():
    yield"first"
    yield"second"
    yield"third"

for x in spam():
    print(x)

-------结果如下---------
first
second
third

进行函数调用的时候,返回一个生成器对象。在使用next()调用的时候,遇到yield就返回,记录此时的函数调用位置,下次调用next()时,从断点处开始。

我们完全可以像使用迭代器一样使用 generator ,当然除了定义。定义一个迭代器,需要分别实现 iter() 方法和 next() 方法,但 generator 只需要一个小小的yield。

generator还有 send() 和 close() 方法,都是只能在next()调用之后,生成器处于挂起状态时才能使用的。

python是支持协程的,也就是微线程,就是通过generator来实现的。配合generator我们可以自定义函数的调用层次关系从而自己来调度线程。

Python中装饰器的相关知识

装饰器允许通过将现有函数传递给装饰器,从而向现有函数添加一些额外的功能,该装饰器将执行现有函数的功能和添加的额外功能。

装饰器本质上还是一个函数,它可以让已有的函数不做任何改动的情况下增加功能。

接下来我们使用一些例子来具体说明装饰器的作用:
如果我们不使用装饰器,我们通常会这样来实现在函数执行前插入日志:

def foo():
    print('i am foo')

def foo():
    print('foo is running')
    print('i am foo')

虽然这样写是满足了需求,但是改动了原有的代码,如果有其他的函数也需要插入日志的话,就需要改写所有的函数,这样不能复用代码。
我们可以进行如下改写:

import logging

def use_log(func):
    logging.warning("%s is running" % func.__name__)
    func()

def bar():
    print('i am bar')

use_log(bar)    #将函数作为参数传入

-------------运行结果如下--------------
WARNING:root:bar is running
i am bar

其中,use_log函数就是装饰器,它把我们真正想要执行的函数bar()封装在里面,返回一个封装了加入代码的新函数,看起来就像是bar()被装饰了一样。

但是这样写还是不够隐式,我们可以通过@语法糖来起到bar = use_log(bar)的作用。

import logging

def use_log(func):
    def wrapper(*args, **kwargs):
        logging.warning('%s is running' % func.__name__)
        return func(*args, **kwargs)

    return wrapper


@use_log
def bar():
    print('I am bar')


@use_log
def haha():
    print('I am haha')


bar()
haha()

------------结果如下------------
WARNING:root:bar is running
I am bar
WARNING:root:haha is running
I am haha

Python是解释语言还是编译语言?

Python是解释语言

解释语言的优点是可移植性好,缺点是运行需要解释环境,运行起来比编译语言要慢,占用的资源也要多一些,代码效率低。

编译语言的优点是运行速度快,代码效率高,编译后程序不可以修改,保密性好。缺点是代码需要经过编译才能运行,可移植性较差,只能在兼容的操作系统上运行。
在这里插入图片描述

什么是元组

Python的元组(tuple)与列表类似,不同之处在于元组的元素不能修改元组使用圆括号包含元素,而列表使用方括号包含元素。元组的创建,只需在圆括号中添加元素并使用逗号分开即可。
与字符串的索引类似,元组的索引也是从0开始。

1.访问元组

可以使用下标索引来访问元组中元素,通过一个案例演示 :

tuplel_demo = ('hello', 100, 4.5)
print(tuplel_demo[0])
print(tuplel_demo[1])
print(tuplel_demo[2])

程序输出结果如下:
在这里插入图片描述

1.2修改元组

元组中的元素值是不允许修改的,但是我们可以对元组进行连接组合。
通过下列案列演示:

tuple_one = (12,34.56)
tuple_two = ('abc', 'xyz')
#以下修改元组操作是非法的
#tuple_one[0] = 100
#创建一个新元组
tuple_three = tuple_one + tuple_two
print(tuple_three) 

上述,创建了两个元组,之后使用+运算符连接这两个元组,生成一个新元组。运行结果如下:
在这里插入图片描述

3.元组的遍历

通过 for 循环可以遍历元组的元素。
通过下列案例演示:

tuple_demo = (1,2,3,4,5)
for number in tuple_demo:
    print(number,end="")

运行结果如下:
在这里插入图片描述

4.元组内置函数

Python 提供的元组内置函数如下表:
在这里插入图片描述

Python中元组和列表的区别:

1、列表是可变的,在创建之后可以对其进行任意的修改。
2、元组是不可变的,元组一旦创建,便不能对其进行更改,可以元组当作一个只读版本的列表。
3、元组无法复制。
4、Python将低开销的较大的块分配给元组,因为它们是不可变的。对于列表则分配小内存块。与列表相比,元组的内存更小。当你拥有大量元素时,元组比列表快

Python中dict(字典)的底层结构?

Python的dict(字典)为了支持快速查找使用了哈希表作为底层结构,哈希表平均查找时间复杂度为O(1)。CPython 解释器使用二次探查解决哈希冲突问题。

Python中的类

Python中使用class关键字来定义类,类要遵循下述格式(模板)。
在这里插入图片描述这里有一个特殊的__init__方法,这是进行初始化的方法,也称为构造函数,只在生成类的实例时被调用一次。
此外,在方法的第一个参数中明确地写入表示自身(自身的实例)的self是Python的一大特点。

下面就举一个例子:
在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/toCVer/article/details/126028741