C语言是一种通用计算机(高级)编程语言;面向过程;广泛应用于计算机系统设计以及应用程序编写;设计目标,是提供一种能以简易的方式编译、处理低级存储器、产生少量的机器码以及不需要任何运行环境支持便能运行的编程语言。
C++与C语言有着紧密的关联性。 事实上,C++可以看作是对C语言的扩展,提供了更多的功能和抽象层次。C++保留了C语言的核心特性,如高效性、直接内存访问和底层控制,同时引入了面向对象编程的概念,使得开发者能够更好地组织和管理代码。此外,C++还借鉴了其他编程语言的一些概念和特性,例如Java的垃圾回收机制和Python的动态类型。然而,与其他语言相比,C++在性能和底层操作方面具有独特的优势。
C++与其他编程语言在基本语法和语义方面存在一些差异。在本节中,我们将比较C++与其他语言在变量声明、控制流、函数定义等方面的差异。这将帮助开发者更好地理解C++的语法和语义,并能够顺利地将已有的编程知识应用到C++中。
1. 变量声明
在C++中,变量的声明需要指定其类型,并且可以选择性地进行初始化。与其他动态类型语言相比,如Python和JavaScript,C++是一门静态类型语言,变量在声明时需要明确指定其类型。例如,以下是一个C++中整型变量的声明和初始化的示例:
int num = 10;
与之相比,一些动态类型语言允许在变量声明时省略类型,并根据赋值自动推断类型。
2. 控制流
C++的控制流语句与其他语言的差异在于语法和语义上的细微差别。例如,C++使用大括号 {} 来定义代码块,而其他语言可能使用缩进或关键字来表示代码块。此外,C++中的条件语句使用关键字 if、else 和 switch,循环语句使用关键字 for、while 和 do-while。虽然这些控制流语句的基本概念相似,但具体的语法和语义可能有所不同。
3. 函数定义
C++的函数定义与其他语言的函数定义也存在一些差异。C++使用函数头和函数体的组合来定义函数。函数头包括返回类型、函数名和参数列表,而函数体则包含实际的函数实现。
在C++中,函数可以被重载,这意味着可以定义具有相同名称但不同参数列表的多个函数。编译器根据调用时提供的参数数量和类型来确定要调用的具体函数。以下是一个函数重载的示例:
// 重载的函数 add
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
在上面的示例中,我们定义了两个名为 add 的函数,一个接收两个整型参数,另一个接收两个浮点型参数。根据调用时提供的参数类型,编译器会自动选择调用合适的函数。
此外,C++还支持默认参数,这意味着在函数定义中可以为某些参数提供默认值。如果调用函数时没有显式提供这些参数的值,将使用默认值。以下是一个具有默认参数的函数示例:
// 带有默认参数的函数
void printMessage(string message, int times = 1) {
for (int i = 0; i < times; i++) {
cout << message << endl;
}
}
在上述示例中,我们定义了一个名为 printMessage 的函数,它接收一个字符串参数 message 和一个整型参数 times,times 参数有默认值为 1。这意味着如果在调用函数时省略了 times 参数,它将默认为 1。
当涉及C++与其他编程语言的基本语法和语义差异时,还有许多方面可以进行比较。以下是一些常见的差异和示例代码:
4. 数组和容器
C++与一些高级语言在数组和容器的表示和使用上存在差异。例如,与Python的动态列表相比,C++使用数组来存储和访问一组元素。以下是一个示例,展示了如何在C++中声明和使用数组:
int numbers[] = {1, 2, 3, 4, 5};
int size = sizeof(numbers) / sizeof(numbers[0]);
for (int i = 0; i < size; i++) {
cout << numbers[i] << " ";
}
另一方面,C++还提供了丰富的容器类,如向量(vector)、链表(list)、映射(map)等。与其他语言相比,C++的容器类提供了更多的底层控制和性能优化。以下是一个使用C++向量的示例:
#include <vector>
using namespace std;
vector<int> numbers = {1, 2, 3, 4, 5};
for (int i = 0; i < numbers.size(); i++) {
cout << numbers[i] << " ";
}
5. 内存管理
C++与许多高级语言在内存管理方面存在显著差异。在C++中,开发者需要显式地管理内存分配和释放。C++提供了操作符 new 和 delete 用于动态分配和释放内存。以下是一个使用 new 操作符动态分配内存的示例:
int* ptr = new int; // 动态分配一个整型变量的内存空间
*ptr = 10;
// 使用动态分配的内存
cout << *ptr << endl;
// 释放内存
delete ptr;
相比之下,一些高级语言如Java和C#提供了自动内存管理,通过垃圾回收机制自动释放不再使用的内存。
6. 异常处理
C++与其他一些语言在异常处理机制上存在差异。在C++中,异常处理使用 try、catch 和 throw 来捕获和处理异常。以下是一个示例,展示了如何在C++中使用异常处理:
try {
// 可能引发异常的代码
throw runtime_error("Something went wrong."); // 抛出异常
}
catch (const exception& ex) {
// 捕获并处理异常
cout << "Exception: " << ex.what() << endl;
}
与之相比,其他一些语言可能使用不同的语法和机制来处理异常,如Java中的 try、catch、finally 块。
接下来是C中的面试知识总结:
【二十四】C/C++中面向对象的相关知识
面向对象程序设计(Object-oriented programming,OOP)有三大特征 ——封装、继承、多态。
封装:把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
关键字:public, protected, private。不写默认为 private。
-
public 成员:可以被任意实体访问。
-
protected 成员:只允许被子类及本类的成员函数访问。
-
private 成员:只允许被本类的成员函数、友元类或友元函数访问。
继承:基类(父类)——> 派生类(子类)
多态:即多种状态(形态)。简单来说,我们可以将多态定义为消息以多种形式显示的能力。多态是以封装和继承为基础的。
C++ 多态分类及实现:
-
重载多态(Ad-hoc Polymorphism,编译期):函数重载、运算符重载
-
子类型多态(Subtype Polymorphism,运行期):虚函数
-
参数多态性(Parametric Polymorphism,编译期):类模板、函数模板
-
强制多态(Coercion Polymorphism,编译期/运行期):基本类型转换、自定义类型转换
【二十五】C/C++中struct的内存对齐与内存占用计算?
什么是内存对齐?计算机系统对基本类型数据在内存中存放的位置有限制,它们会要求这些数据的首地址的值是有效对齐值的倍数。
什么是有效对齐值?计算机系统有默认对齐系数n,可以通过#pragma pack(n)来指定。有效对齐值就等与该对齐系数和结构体中最长的数据类型的长度两者最小的那一个值,比如对齐系数是8,而结构体中最长的是int,4个字节,那么有效对齐值为4。
为什么要内存对齐?假如没有内存对齐机制,数据可以任意存放,现在一个int变量存放在从地址1开始的连续四个字节地址中。当4字节存取粒度的处理器去取数据时,要先从0地址开始读取第一个4字节块,剔除不想要的字节(0地址),然后从地址4开始读取下一个4字节块,同样剔除不要的数据(5,6,7地址),最后留下的两块数据合并放入寄存器,这需要做很多工作,整体效率较低。
struct内存占用如何计算?结构体的内存计算方式遵循以下规则:
-
数据成员对齐规则:第一个数据成员放在offset为0的地方,以后的每一个成员的offset都必须是该成员的大小与有效对齐值相比较小的数值的整数倍,例如第一个数据成员是int型,第二个是double,有效对齐值为8,所以double的起始地址应该为8,那么第一个int加上内存补齐用了8个字节
-
结构体作为成员:如果一个结构里有某些结构体成员,则结构体成员要从其内部有效对齐值的整数倍地址开始存储。(比如struct a中存有struct b,b里有char, int, double,那b应该从8的整数倍开始存储)
-
结构体内存的总大小,必须是其有效对齐值的整数倍,不足的要补齐。
我们来举两个🌰:
#include <stdio.h>
#pragma pack(8)
int main()
{
struct Test
{
int a;
//long double大小为16bytes
long double b;
char c[10];
};
printf("%d", sizeof(Test));
return 0;
}
struct的内存占用为40bytes
#include <stdio.h>
#pragma pack(16)
int main()
{
struct Test
{
int a;
//long double大小为16bytes
long double b;
char c[10];
}
printf("%d", sizeof(Test));
return 0;
}
struct的内存占用为48bytes
【二十六】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++程序开发过程的第一步,它主要包括程序文本的输入和修改。任何一种文本编辑器都可以完成这项工作。当用户完成了C++程序的编辑时,应将输入的程序文本保存为以.cpp为扩展名的文件(保存C++头文件时应以.h为扩展名)。
编译:C++是一种高级程序设计语言,它的语法规则与汇编语言和机器语言相比更接近人类自然语言的习惯。然而,计算机能够“看”懂的唯一语言是汇编语言。因此,当我们要让计算机“看”懂一个C++程序时,就必须使用编译器将这个C++程序“翻译”成汇编语言。编译器所做的工作实际上是一种由高级语言到汇编语言的等价变换。
汇编:将汇编语言翻译成机器语言指令。汇编器对汇编语言进行一系列处理后最终产生的输出结构称为目标代码,它是某种计算机的机器指令(二进制),并且在功能上与源代码完全等价。保存源代码和目标代码的文件分别称为源文件和目标文件( .obj)。
链接:要将汇编器产生的目标代码变成可执行程序还需要最后一个步骤——链接。链接工作是由“链接器”完成的,它将编译后产生的一个或多个目标文件与程序中用到的库文件链接起来,形成一个可以在操作系统中直接运行的可执行程序。(linux中的.o文件)
运行和调试:我们接下来就可以执行程序了。如果出现问题我们可以进行调试debug。
【二十八】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++中野指针的概念?
野指针也叫空悬指针,不是指向null的指针,是未初始化或者未清零的指针。
产生原因:
-
指针变量未及时初始化。
-
指针free或delete之后没有及时置空。
解决办法:
-
定义指针变量及时初始化活着置空。
-
释放操作后立即置空。
【三十一】C/C++中内存泄漏以及解决方法?
内存泄漏是指己动态分配的堆内存由于某种原因导致程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
解决方法:
造成内存泄漏的主要原因是在使用new或malloc动态分配堆上的内存空间,而并未使用delete或free及时释放掉内存造成的。所以解决方法就是注意new/delete和malloc/free一定要配套使用。
【三十二】C/C++中面向对象和面向过程的区别?
面向对象(Object Oriented Programming,OOP)编程模型首先抽象出各种对象(各种类),并专注于对象与对象之间的交互,对象涉及的方法和属性都封装在对象内部。
面向对象的编程思想是一种依赖于类和对象概念的编程方式,一个形象的例子是将大象装进冰箱:
- 冰箱是一个对象,大象也是一个对象。
- 冰箱有自己的方法,打开、存储、关闭等;大象也有自己的方法,吃、走路等。
- 冰箱有自己的属性:长、宽、高等;大象也有自己的属性:体重、高度、体积等。
面向过程(Procedure Oriented Programming,POP)编程模型是将问题分解成若干步骤(动作),每个步骤(动作)用一个函数来实现,在使用的时候,将数据传递给这些函数。
面向过程的编程思想通常采用自上而下、顺序执行的方式进行,一个形象的例子依旧是将大象装进冰箱:
- 打开冰箱。
- 把大象装进冰箱。
- 关闭冰箱。
面向对象和面向过程的区别:
-
安全性角度。面向对象比面向过程安全性更高,面向对象将数据访问隐藏在了类的成员函数中,而且类的成员变量和成员函数都有不同的访问属性;而面向过程并没有办法来隐藏程序数据。
-
程序设计角度。面向过程通常将程序分为一个个的函数;而面向对象编程中通常使用一个个对象,函数通常是对象的一个方法。
-
逻辑过程角度。面向过程通常采用自上而下的方法;而面向对象通常采用自下而上的方法。
-
程序扩展性角度。面向对象编程更容易修改程序,更容易添加新功能。
【三十三】C/C++中常用容器功能汇总
vector(数组)
vector是封装动态数组的顺序容器。
成员函数:
- at():所需元素值的引用。
- front():访问第一个元素(返回引用)。
- back():访问最后一个元素(返回引用)。
- beign():返回指向容器第一个元素的迭代器。
- end():返回指向容器末尾段的迭代器。
- empty():检查容器是否为空。
- size():返回容器中的元素数。
- capacity():返回当前存储空间能够容纳的元素数。
- clear():清除内容。
- insert():插入元素。
- erase():擦除元素。
- push_back():将元素添加到容器末尾。
- pop_back():移除末尾元素。
- *max_element(v.begin(), v.end()):返回数组最大值。
- *min_element(v.begin(), v.end()):返回数组最小值。
queue(队列)
queue是容器适配器,他是FIFO(先进先出)的数据结构。
成员函数:
- front():访问第一个元素(返回引用)。
- back():访问最后一个元素(返回引用)。
- empty():检查容器是否为空。
- size():返回容器中的元素数。
- push():向队列尾部插入元素。
- pop():删除首个元素。
deque(双端队列)
deque是有下标顺序容器,它允许在其首尾两段快速插入和删除。
成员函数:
- front():访问第一个元素(返回引用)。
- back():访问最后一个元素(返回引用)。
- beign():返回指向容器第一个元素的迭代器。
- end():返回指向容器末尾段的迭代器。
- empty():检查容器是否为空。
- size():返回容器中的元素数。
- clear(): 清除内容。
- insert():插入元素。
- erase():擦除元素。
- push_back():将元素添加到容器末尾。
- pop_back():移除末尾元素。
- push_front():插入元素到容器起始位置。
- pop_front():移除首元素。
- at():所需元素值的引用。
set(集合)
集合基于红黑树实现,有自动排序的功能,并且不能存放重复的元素。
成员函数:
-
begin()–返回指向第一个元素的迭代器。
-
clear()–清除所有元素。
-
count()–返回某个值元素的个数。
-
empty()–如果集合为空,返回true。
-
end()–返回指向最后一个元素的迭代器。
-
erase()–删除集合中的元素。
-
find()–返回一个指向被查找到元素的迭代器。
-
insert()–在集合中插入元素。
-
size()–集合中元素的数目。
unordered_set(无序集合)
无序集合基于哈希表实现,不能存放重复的元素。元素类型必须可以比较是否相等,因为这可以确定元素什么时候相等。
成员函数:
- empty():检查容器是否为空。
- size():返回容器中的元素数。
- insert():插入元素。
- clear():清除内容。
- count():返回匹配特定键的元素数量。
- find():寻找带有特定键的元素。
- erase()–删除集合中的元素。
unordered_map
unordered_map是关联容器,含有带唯一键的键-值对。
搜索、插入和元素移除拥有平均常数时间复杂度。
元素在内部不以任何特定顺序排序,而是组织进桶中。元素放进哪个桶完全依赖于其键的哈希。这允许对单独元素的快速访问,因为一旦计算哈希,则它准确指代元素所放进的桶。
成员函数:
- empty():检查容器是否为空。
- size():返回可容纳的元素数。
- insert():插入元素。
- clear():清除内容。
- count():返回匹配特定键的元素数量。
- find():寻找带有特定键的元素。
- erase()–删除集合中的元素。
【三十四】C/C++中指针和引用的区别
C语言的指针让我们拥有了直接操控内存的强大能力,而C++在指针基础上又给我们提供了另外一个强力武器 → \to →引用。
首先我们来看一下C++中对象的定义:对象是指一块能存储数据并具有某种类型的内存空间。
一个对象a,它有值和地址&a。运行程序时,计算机会为该对象分配存储空间,来存储该对象的值,我们通过该对象的地址,来访问存储空间中的值。
指针p也是对象,它同样有地址&p和存储的值p,只不过,p存储的是其他对象的地址。如果我们要以p中存储的数据为地址,来访问对象的值,则要在p前加引用操作符 ∗ * ∗,即 ∗ p *p ∗p。
对象有常量(const)和变量之分,既然指针本身是对象,那么指针所存储的地址也有常量和变量之分,指针常量是指,指针这个对象所存储的地址是不可改变的,而常量指针的意思就是指向常量的指针。
我们可以把引用理解成变量的别名。定义一个引用的时候,程序把该引用和它的初始值绑定在一起,而不是拷贝它。计算机必须在声明引用r的同时就要对它初始化,并且r一经声明,就不可以再和其他对象绑定在一起了。
实际上,我们也可以把引用看作是通过一个指针常量来实现的,指向的地址不变,地址里的内容可以改变。
接下来我们来看看指针和引用的具体区别:
- 指针是一个新的变量,要占用存储空间,存储了另一个变量的地址,我们可以通过访问这个地址来修改另一个变量。而引用只是一个别名,还是变量本身,不占用具体存储空间,只有声明没有定义。对引用的任何操作就是对变量本身进行操作,以达到修改变量的目的。
- 引用只有一级,而指针可以有多级。
- 指针传参的时候,还是值传递,指针本身的值不可以修改,需要通过解引用才能对指向的对象进行操作。引用传参的时候,传进来的就是变量本身,因此变量可以被修改。
- 引用它一定不为空,因此相对于指针,它不用检查它所指对象是否为空,这样就提高了效率。
- 引用必须初始化,而指针可以不初始化。
我们可以看下面的代码:
int a,b,*p,&r=a;//正确
r=3;//正确:等价于a=3
int &rr;//出错:引用必须初始化
p=&a;//正确:p中存储a的地址,即p指向a
*p=4;//正确:p中存的是a的地址,对a所对应的存储空间存入值4
p=&b//正确:p可以多次赋值,p存储b的地址
“&”不仅能表示引用,还可以表示成地址,还有可以作为按位与运算符。这个要根据具体情况而定。比如上面的例子,等号左边的,被解释为引用,右边的被解释成取地址。
引用的操作加了比指针更多的限制条件,保证了整体代码的安全性和便捷性。引用的合理使用可以一定程度避免“指针满天飞”的情况,可以一定程度上提升程序鲁棒性。并且指针与引用底层实现都是一样的,不用担心两者的性能差距。
【三十五】C/C++中宏定义的相关知识
宏定义可以把一个名称指定成任何一个文本。在完成宏定义后,无论宏名称出现在源代码的何处,预处理器都会将其替换成指定的文本。
//define 宏名 文本
#define WeThinkIn 666688889999
//define 宏名(参数) 文本
#define R(a,b) (a/b)
//注:带参数的宏替换最好在表达式整体上加括号,避免结果受其他运算影响。
宏定义的优点:
- 方便程序修改,如果一个常量在程序中大量使用,我们可以使用宏定义为其设置一个标识符。当我们想修改这个常量时,直接修改宏定义处即可,不必在程序中海量寻找所有相关位置。
- 提高程序的运行效率,使用带参数的宏定义可以完成函数的功能,但同时又比函数节省系统开销,提升程序运行效率。(无需调用函数这个流程)
宏定义和函数的区别:
- 宏在预处理阶段完成替换,之后替换的文本参与编译,相当于是恒等代换过程,运行时不存在函数调用,执行起来更快;而函数调用在运行时需要跳转到具体调用函数。
- 宏定义没有返回值;函数调用具有返回值。
- 宏定义参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
- 宏定义不是说明或者语句,结尾不用加分号。
- 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用# undef命令;而函数作用域在函数调用处。
【三十六】C/C++中typedef关键字的相关知识
我们可以使用typedef关键字来定义自己习惯的数据类型名称,来替代系统默认的基本类型名称以及其他类型等名称。
在工业界中,我们一般在如下两个场景中会见到typedef的身影。
// 1.为基本数据类型定义新的类型名
typedef unsigned int WeThinkIn_int;
typedef char* WeThinkIn_point;
// 2.为自定义数据类型(结构体、共用体和枚举类型)定义简洁的类型名称
typedef struct target_Object
{
int x;
int y;
} WeThinkIn_Object;
typedef与宏定义的区别:
- 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
- 宏替换发生在预处理阶段,属于文本恒等替换;typedef是编译中发挥作用。
- 宏定义参数没有类型,不进行类型检查;typedef参数具有类型,需要检查类型。
- 宏不是语句,不用在最后加分号;typedef是语句,要加分号标识结束。
- 注意对指针的操作,typedef char * p_char和#define p_char char *区别巨大。