自己在秋招找工作过程中遇到的一些C/C++面试题,大中小厂都有,分享出来,希望能帮到有缘人。
函数原型为int snprintf(char *str, size_t size, const char *format, …)
两点注意:
(1) 如果格式化后的字符串长度 < size,则将此字符串全部复制到str中,并给其后添加一个字符串结束符(‘\0’);
(2) 如果格式化后的字符串长度 >= size,则只将其中的(size-1)个字符复制到str中,并给其后添加一个字符串结束符(‘\0’),返回值为欲写入的字符串长度。
snprintf的返回值是欲写入的字符串长度abcdefghijk,而不是实际写入的字符串度。如:
char buf[8];
int n = snprintf(buf, 5, "abcdefghijk");
printf("n %d buf %s\n", n, buf);
运行结果为:
n 11 buf abcd
注意这个结果,只输出了abcd,长度为4,不是期待的5,没有输出 e ,说明snprintf函数最后自动加上去的’\0’,是算在size内部的,是格式化字符串的总长度(不包括’\0’),这里使用sizeof(buf)时需要注意+1,这一点与malloc申请空间类似。
1.snprintf会自动在格式化后的字符串尾添加\0,结尾符是包含在size长度内部的。
2.snprintf会在结尾加上\0,不管buf空间够不够用,所以不必担心缓冲区溢出。
3.snprintf的返回值n,当调用失败时,n为负数,当调用成功时,n为格式化的字符串的总长度(不包括\0),当然这个字符串有可能被截断,因为buf的长度不够放下整个字符串。
可以判断输出
if ( n < 0) : snprintf出错了
if ( n >0 && n < sizeof(buf) ) : snprintf成功,并且格式了完成的字符串。
if ( n >= sizeof(buf) ) : snprintf成功,但要格式化的字符串被截断了。
没有内置的机制来直接实现类和多态。可以使用结构体和函数指针来模拟类和多态的概念。具体步骤如下:
typedef struct {
// 类中的成员变量
// 类中的虚函数指针
void (*polymorphicFunction)();
} Base;
typedef struct {
Base base; // 继承基类
// 派生结构体中的成员变量
} Derived;
void derivedPolymorphicFunction() {
// 派生结构体中重写的虚函数
}
// 创建派生结构体对象
Derived derivedObj;
// 将函数指针指向派生结构体的重写函数
base.polymorphicFunction = derivedPolymorphicFunction;
// 通过基类指针调用虚函数,实现多态
base.polymorphicFunction();
用于在内存中移动一段数据,它会处理源地址和目标地址重叠的情况
#include
void *memmove(void *dest, const void *src, size_t n) {
unsigned char *d = dest;
const unsigned char *s = src;
if (d == s) {
// Nothing to do if source and destination are the same
return dest;
}
else if(d < s || d >= (s + n)) {
// Non-overlapping memory, safe to copy from start to end
while (n--) {
*d++ = *s++;
}
}
else {
// Overlapping memory, copy from end to start
d += n;
s += n;
while (n--) {
*(--d) = *(--s);
}
}
return dest;
}
在内存之间进行字节级别的复制
void my_memcpy(void* dest, const void* src, size_t size) {
unsigned char* d = (unsigned char*)dest;
const unsigned char* s = (const unsigned char*)src;
for (size_t i = 0; i < size; i++) {
d[i] = s[i];
}
}
#include
int my_strcmp(const char* str1, const char* str2) {
while (*str1 != '\0' || *str2 != '\0') {
if (*str1 != *str2) {
return (*str1 - *str2);
}
str1++;
str2++;
}
return 0; // 字符串相等
}
int main() {
const char* str1 = "Hello";
const char* str2 = "Hello";
int result = my_strcmp(str1, str2);
if (result == 0) {
printf("Strings are equal.\n");
} else if (result < 0) {
printf("str1 is less than str2.\n");
} else {
printf("str1 is greater than str2.\n");
}
return 0;
}
1)调用者保存当前函数的上下文
2)传递参数
3) 跳转到被调用的函数
4)创建栈帧:保存局部变量,参数,返回地址等信息
5)执行函数体
6)返回
7)恢复调用者的上下文
栈指针(通常是寄存器,如ESP或RSP)用于管理栈上的数据
strcpy:比较2个字符串
strlen:算字符串长度
strcat:拼接字符串
strcmp:字符串的比较
strncpy:可以指定比较的字符
strstr:在一个字符串中查找另一个字符串第一次出现
strchr: 在一个字符串中查找某个字符的第一次出现
struct里面的成员在内存中各自占有独立的空间,大小=所有成员的大小之和,可以同时访问结构体中的多个成员,用于多个相关的数据字段,比如人:姓名,年龄,性别
union 里面的成员是共用内存的,大小=最大成员的大小,用于节约内存,一个数据包:文本/图像/音频,只需要存储其中一种数据类型
struct默认公有,类成员可以外部访问,默认继承是公有
class默认私有,类内访问,或者使用公有接口,默认继承也是私有
都可以定义友元关系,外部类或函数可以访问私有成员
减少内存碎片,CPU更加高效的访问成员
1)结构体的对齐要求是其成员中最大的数据类型的对齐要求,比如int和char,按照int对齐
2)结构体的大小必须是其对齐要求的整数倍
内存对齐(Memory Alignment)是指在内存中分配变量时,将其存储在地址是特定值的倍数上的要求。这是由于硬件的要求,以提高内存读取和写入操作的效率。
在计算机系统中,数据的读取和写入通常是以内存块为单位进行的。为了使这些操作更高效,许多计算机体系结构要求特定类型的数据在特定的地址上对齐。具体的对齐要求取决于数据类型和硬件架构。
常见的对齐要求包括字节对齐和字对齐。字节对齐要求数据的起始地址必须是数据类型的大小的整数倍。例如,一个整型变量(4字节)通常需要在地址是4的倍数的位置上对齐。字对齐要求数据的起始地址必须是数据类型大小的整数倍,并且整个数据的大小也必须是数据类型大小的整数倍。
内存对齐的好处包括:
C++引入了new运算符是为了更好地支持对象的动态内存分配和构造。虽然C语言中有malloc函数可以进行内存分配,但它只是简单地分配一块内存空间,并不会调用对象的构造函数。
C++的new运算符除了分配内存外,**还会调用对象的构造函数来初始化对象。**这是因为C++是面向对象的语言,强调对象的生命周期管理和构造/析构过程。通过new运算符,我们可以在进行内存分配的同时,自动调用对象的构造函数进行初始化。
C++的new运算符还提供了更多的功能,如对数组的动态内存分配和构造,异常处理机制等。它可以根据对象的类型进行动态内存分配,并返回相应类型的指针,而不需要显式指定分配的字节数。
C++的new运算符还可以与delete运算符搭配使用,用于释放对象占用的内存空间,并在适当的时候调用析构函数进行资源的释放。而C语言中的malloc和free则是对应的内存分配和释放函数,不会自动调用对象的构造和析构函数。
C++引入new运算符是为了提供更方便、安全和面向对象的内存分配和初始化机制。
内存池是一种用于管理和分配内存的技术,旨在提高内存分配和释放的效率,减少动态内存分配的开销。
应用程序需要频繁地进行内存的分配和释放,而动态内存分配操作(如malloc或new)在频繁使用时可能会产生较大的开销。内存池通过预先分配一块较大的内存空间,并将其划分为多个固定大小的内存块(或称为内存块池)。应用程序可以从内存池中获取这些固定大小的内存块,而无需频繁地进行动态内存分配操作。
内存池可以分为静态内存池和动态内存池两种类型:
内存池是一种常用的内存管理技术,适用于需要频繁进行内存分配和释放的场景,如游戏引擎、网络服务器等。
在STL中,内存池被实现为一个名为std::allocator的模板类。std::allocator提供了内存分配和释放的接口,并可以与其他STL容器(如std::vector、std::list等)配合使用。
std::allocator内存池的主要目的是优化STL容器的性能。STL容器在进行元素的插入、删除等操作时,需要频繁地进行内存分配和释放。为了避免频繁的动态内存分配操作带来的性能开销,allocator使用了内存池的技术。
具体来说,std::allocator维护了一个内存池**,该内存池中预先分配了一定数量的内存块**,每个内存块的大小为容器元素的大小。当容器需要进行内存分配时,std::allocator会首先从内存池中查找是否有可用的内存块。如果有可用的内存块,则直接分配其中一个内存块;如果没有可用的内存块,则会向系统申请新的内存。
使用std::allocator的好处是可以避免频繁的动态内存分配和释放,从而提高容器操作的性能。此外,std::allocator还可以自定义内存池的实现,以适应特定的需求。
需要注意的是,std::allocator内存池是对STL容器的默认分配器,但并不是所有的STL容器都使用内存池。一些容器(如std::vector)使用了std::allocator内存池进行内存管理,而其他容器(如std::map)可能使用不同的内存管理策略。这取决于具体的实现和编译器。
总结起来,STL中的内存池通过std::allocator提供了一种优化STL容器性能的机制。它通过预先分配一些内存块,并在容器操作时避免频繁的动态内存分配和释放,从而提高了容器操作的效率。
内存泄漏(Memory Leak)是指在程序运行中,由于错误的内存管理导致已经分配的内存无法被正确释放,从而导致系统中的可用内存逐渐减少,最终耗尽所有可用内存资源。
内存泄漏通常发生在动态内存分配的场景下,如使用malloc、new等操作分配内存空间后,未能正确释放该内存空间。内存泄漏可能是因为以下几种情况:
malloc、new)分配了一块内存空间后,如果没有调用相应的内存释放函数(如free、delete)来释放该内存空间,就会导致内存泄漏。,使用方式为void* malloc(size_t size),返回一个void*指针。而new是C++的运算符,使用方式为new Type,返回一个指向Type类型的指针。std::bad_alloc异常。而malloc只是简单地分配一块内存空间,不会进行类型检查。free不提供类型检查,delete提供更好的内存安全性
free只释放内存,不析构对象,delete是先析构对象,再释放内存
内存泄漏是指在程序运行过程中,分配的内存空间没有被释放,导致这些内存无法再被程序访问和利用,从而造成内存的浪费。内存泄漏可能会导致程序的性能下降、资源耗尽以及不可预测的行为。下面是一些处理内存泄漏的常见方法:
std::unique_ptr和std::shared_ptr,它们可以自动管理动态分配的内存,当指针不再需要时会自动释放内存。使用智能指针可以避免手动释放内存的繁琐,并减少内存泄漏的风险。new运算符分配内存后,需要在不再需要该内存时使用delete运算符显式释放内存。确保每次动态分配内存后,都有相应的delete语句用于释放内存。以下是一些方法来避免内存碎片:
指针没有初始化,指向非法内存
野指针:指向无效内存,指针释放后没有重置为nullptr
悬空指针:未初始化
内存泄漏:忘记释放内存,使用智能指针
内存溢出:程序访问未分配的内存区域,栈溢出:递归深度过深,堆溢出:访问未分配,缓冲区溢出:写入大于目标缓冲区的大小,覆盖相邻内存区域
主要由栈区、堆区、静态/全局存储区和常量存储区组成
栈区:先进后出的数据结构,一段连续区域,每个函数都会创建一个称为栈帧的区域,用于存储函数参数、返回地址、函数局部变量,内存分配是自动的,函数调用结束自动销毁
堆区:动态分配内存的区域,大小并不固定,一个new对应一个delete,或者一个malloc对应一个free来管理
在程序任何地方都可以进行访问,但是需要手动管理内存
静态/全局存储区:存储全局变量和静态变量
全局变量在程序开始时创建,程序结束时释放
静态变量的作用域可以是全局的(函数外部定义)或局部的(函数内部定义,存储在栈帧中),
常量存储区:存放一些常量
具体实现可能因编译器、操作系统和运行环境而异。C++还支持其他一些特殊的存储区域
如自由存储区:动态分配内存的另一个区域
堆:手动分配和释放,new-delete,malloc-free
栈: 自动分配的,进入一个函数,分配一块内存空间,函数结束自动释放
栈:连续区域
堆:不一定连续
栈:一般是1M
堆:一般是4GB
栈:相对较快,以固定大小的栈帧为单位进行
堆:相对较慢,需要在堆中查找足够大的连续内存空间来满足分配请求
1)对象的生命周期需要在函数调用结束后继续存在
2)需要动态分配大量的内存空间,而栈空间有限
3)需要共享数据
4)需要动态的创建对象和销毁
1)对象的生命周期需要在函数调用结束自动销毁
2)需要分配的内存空间较小且固定大小
3)对象不需要函数之间共享或在函数调用之外继续存在
4)需要较快的内存分配和释放速度
堆区的内存分配和释放需要手动管理,需要程序员负责分配和释放内存,避免内存泄漏和内存溢出等问题。
而栈区的内存管理由编译器自动处理,无需手动干预。
在实际编程中,通常会根据具体的需求和情况来选择使用堆区或栈区。需要综合考虑内存大小、对象的生命周期、内存分配速度和内存管理复杂性等因素。
栈内存的分配是由操作系统和编程语言运行时环境控制的。当栈的内存空间满了,系统通常会引发栈溢出错误。栈溢出是一种严重的运行时错误,通常会导致程序终止,因为栈的内存空间是有限的,而栈的大小在程序启动时就已经分配好了。
在栈内存满了的情况下,通常不会自动重新分配栈的内存,因为栈的大小在程序运行时是固定的。重新分配栈内存可能涉及复杂的内存管理和程序状态转移,因此通常不是自动执行的。
一般来说,为了避免栈溢出,开发者应该小心管理递归深度、局部变量和函数参数的大小,确保递归函数具有终止条件,避免无限循环,以及正确处理缓冲区和数据结构,以减小栈的内存消耗。
如果程序需要更大的内存空间来存储大量数据,通常会使用堆内存(通过动态内存分配函数如malloc、new等)来分配和释放内存,而不是依赖栈。堆内存的分配和释放通常需要显式的编程操作,而不会自动发生,因此开发者需要负责管理堆内存。这允许程序在运行时动态地分配和释放内存,而不受栈大小的限制。
1)递归终止条件
2)使用计数器变量跟踪递归的深度
std::allocator:std::allocator 是STL中的默认分配器,用于动态内存分配和释放。它提供了 allocate 和 deallocate 函数来分配和释放内存块,并支持对象的构造和销毁。许多STL容器都使用 std::allocator 作为默认分配器。
一级分配和二级分配是内存管理中的两种常见分配方式,用于将内存分配给应用程序或数据结构。它们的原理如下:
一级分配(First Fit Allocation):
二级分配(Two-Level Allocation):
需要注意的是,实际的内存分配算法可能更复杂,具体取决于操作系统和应用程序的需求。此外,现代操作系统通常还会实施更高级的内存管理策略,如分页和虚拟内存,以提供更灵活和高效的内存管理。内存分配是一个庞大的主题,涉及到多种算法和技术,以满足各种应用和性能需求。
菱形继承是指一个派生类同时继承自两个直接或间接基类,而这两个基类又共同继承自一个共同的基类。这样形成的继承关系图形状类似于菱形
A
/
B C
\ /
D
在这个例子中,类 D 继承自类 B 和类 C,而类 B 和类 C 都继承自类 A。这样,类 D 就有了两个路径可以访问类 A 中的成员。
菱形继承可能导致以下问题:
冗余和二义性:当派生类 D 尝试访问从基类 A 继承的成员时,由于存在两条路径,会导致成员变量和成员函数的冗余,增加了代码的复杂性。此外,如果基类 A 中的成员在类 B 和类 C 中有不同的实现,那么在派生类 D 中访问这些成员时会产生二义性,编译器无法确定具体使用哪个实现。
虚基类解决冗余和二义性:为了解决菱形继承带来的问题,C++ 提供了虚基类(Virtual Base Class)的概念。通过将基类 A 声明为虚基类,派生类 D 就只会有一个从 A 继承的成员副本,避免了冗余和二义性。使用虚基类时,需要使用虚基类语法来声明和初始化虚基类。
菱形继承是多继承中的一个特殊情况,需要谨慎使用,因为它可能引发代码冗余和二义性的问题。在设计多继承的继承关系时,应考虑使用虚基类来解决菱形继承带来的问题,或者重新设计继承结构以避免菱形继承的出现。
在C++中,多态通过虚函数(virtual function)和继承机制来实现。以下是多态的实现步骤:
class Base {
public:
virtual void foo() {
// 基类中的虚函数实现
}
};
2.派生类重写虚函数:在派生类中,可以通过在函数声明前加上 override 关键字来重写基类的虚函数。重写虚函数时,函数签名必须和基类的虚函数完全一致
class Derived : public Base {
public:
void foo() override {
// 派生类中的虚函数实现
}
};
Base* ptr = new Derived(); // 使用基类指针指向派生类对象
ptr->foo(); // 调用虚函数,根据对象的实际类型选择合适的实现
4.当通过基类指针或引用调用虚函数时,实际执行的是派生类中重写的虚函数,即根据对象的实际类型进行动态绑定,实现了多态性。
为了实现多态,必须使用基类指针或引用来操作对象,以允许在运行时根据对象的实际类型来选择虚函数的实现。
如果基类的析构函数是虚函数(通过在析构函数前加上 virtual 关键字),那么在删除派生类对象时,会先调用派生类的析构函数来确保正确释放资源。
虚函数表(Virtual Function Table,简称 VTable)是C++中实现多态的一种机制。
1)当类中存在虚函数时,编译器会为该类生成一个虚函数表,其中存储了该类中所有虚函数的地址。
2)在运行时,当通过指针或引用调用虚函数时,程序会根据对象的实际类型查找到对应的虚函数表,然后通过表中存储的函数地址来调用正确的函数实现。
3)每个含有虚函数的类都有自己的虚函数表,虚函数表通常存储在对象的内存布局中的第一个位置。每个虚函数表中包含了该类所有虚函数的函数指针,每个虚函数在虚函数表中的位置是固定的,因此可以通过偏移量来访问。
4)对于继承关系中的子类,其虚函数表会包含父类中的虚函数,但子类所新增加的虚函数仍会在新的虚函数表中存储。同时,如果子类覆盖了父类的虚函数,那么子类的虚函数表中对应位置的函数指针将指向子类的实现。
1)每个包含虚函数的类都有一个虚函数表,虚函数表是一个数组,存储类中虚函数的地址
2) 对于每个类的实例,编译器在对象的内存布局中插入一个指向该类的虚函数表的指针,称为虚指针
3)对象调用虚函数的时候,实际通过虚指针找到虚函数表,根据函数在表中的索引来调用正确的虚函数,允许在运行期间确定要调用哪个实际函数
4)派生类没有虚函数会从基类继承虚函数表并覆盖
当一个基类指针或引用指向一个派生类对象,并且通过该基类指针或引用删除该对象时,如果基类的析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类中的资源无法正确释放,造成资源泄漏或不一致的状态。
通过将基类的析构函数声明为虚函数,可以解决这个问题。当基类的析构函数是虚函数时,通过基类指针或引用删除对象时,会根据指针或引用实际指向的对象类型调用相应的析构函数。这样,派生类的析构函数也会被正确调用,从而确保派生类中的资源得到正确释放。
class Base {
public:
virtual ~Base() {
// 基类的析构函数声明为虚函数
// ...
}
};
class Derived : public Base {
public:
~Derived() {
// 派生类的析构函数
// ...
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 使用基类指针删除对象
return 0;
}
在上述代码中,当使用基类指针ptr删除对象时,如果基类的析构函数不是虚函数,只会调用基类的析构函数,导致派生类的析构函数不会被调用,从而可能引发问题。但是,如果基类的析构函数被声明为虚函数(virtual ~Base()),那么使用基类指针删除对象时,将会调用派生类的析构函数,确保派生类中的资源得到正确释放。
因此,为了正确实现多态性的销毁,当一个类可能被继承时,建议将其析构函数声明为虚函数。这样可以确保通过基类指针或引用删除对象时,能够调用到派生类的析构函数,从而正确释放资源。
不可以,可以

库:函数的集合 作用:共享代码
静态、动态指链接
程序编译过程中,在链接阶段,程序生成的汇编文件和库进行链接,生成可执行文件。

静态链接库在程序编译时被连接到目标代码中参与编译;链接时将库完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝;生成可执行程序之后,静态链接库不需要(因已将函数拷贝到可执行文件中)
优点:
缺点:
程序运行时由系统动态加载动态库到内存,供程序调用,系统只加载一次,多个程序共用,节省内存
优点:
缺点:
1.时期
静态库在编译时链接,在链接时拷贝
动态库在运行时链接
2.资源
静态库在每次使用时将全部链接进可执行程序,浪费资源。
动态库在使用时访问动态库中函数,节省资源。
3.更新升级
静态库更新,则每个使用该静态库的程序都需要更新,不易于更新升级
动态库仅更新自身,易于更新升级
4.包含其他库
静态链接库不能再包含其他动态链接库
动态链接库可以包含其他动态链接库
预编译:.h,.cpp,.c文件->.i文件
** 展开宏定义,删除注释**
编译:.i文件->.s文件
** 对预编译后的代码进行语法和语义分析,生成汇编代码**
汇编:.s文件->.o文件
** 将汇编代码转成机器可执行的目标文件,包含了机器指令,符号表等**
链接:.o文件->可执行程序.exe,.out
** 将目标文件与所需的库链接,生成最终的可执行文件,解析符号引用,不同目标文件符号连接**
** 最终生成的可执行文件可以直接在操作系统上运行**
静态库:编译时链接,代码合并到可执行文件,优点:加载速度快,可移植性高 缺点:可执行文件大
#include是静态库
动态库:运行时链接,优点:代码重用和共享库,节约空间 缺点:移植性差,性能开销大
一、引用的头文件不同
#include< >引用的是编译器的类库路径里面的头文件。
#include“ ”引用的是你程序目录的相对路径中的头文件。
二、用法不同
#include< >用来包含标准头文件(例如stdio.h或stdlib.h).
#include“ ”用来包含非标准头文件。
三、调用文件的顺序不同
#include< >编译程序会先到标准函数库中调用文件。
#include“ ”编译程序会先从当前目录中调用文件。
四、预处理程序的指示不同
#include< >指示预处理程序到预定义的缺省路径下寻找文件。
#include“ ”指示预处理程序先到当前目录下寻找文件,再到预定义的缺省路径下寻找文件。
过程:词法分析,语法分析,语义分析,生成中间代码
作用:跨平台编译,可维护性,多语言支持,模块化
1. 静态链接库:
#include 是C/C++编程语言中的预处理指令,用于在源代码中包含头文件(header file)。它的作用是将头文件中的声明内容插入到源代码中,以便在编译时使用头文件中定义的函数、结构、宏等。
与动态链接库相比,#include 具有以下区别:
异常处理是一种编程技术,用于在程序执行过程中检测和处理潜在的错误或异常情况。异常是指在程序运行过程中出现的意外或异常的情况,例如除以零、访问不存在的内存地址、文件读取错误等。
C++ 异常处理机制的核心概念如下:
throw 关键字抛出异常。异常对象可以是任意类型,通常是标准库或自定义的异常类的实例。try-catch 块来捕获异常。在 try 块中放置可能抛出异常的代码,并在 catch 块中捕获和处理异常。catch 块中的代码将根据捕获的异常类型执行相应的操作。catch 块中的代码被称为异常处理程序。它会根据捕获的异常类型执行相应的逻辑,例如输出错误消息、进行日志记录、进行资源清理等。try:用于包裹可能抛出异常的代码块。在 try 块中,如果发生异常,控制流将转到匹配的 catch 块。catch:用于捕获和处理异常。catch 后面可以跟一个或多个异常类型,用于指定要捕获的异常类型。当异常类型匹配时,相应的 catch 块将执行。throw:用于抛出异常。throw 后面可以跟一个异常对象,将该异常对象抛出。std::exception:是 C++ 标准库中定义的基类,用于表示异常的基本类。可以自定义继承自 std::exception 的异常类来表示特定的异常情况。在 C++ 中,有几种类型的异常可以使用。以下是 C++ 异常的常见类型:
std::exception 类派生而来的。这些异常类用于表示常见的异常情况,可以用于捕获和处理不同类型的异常。一些常见的标准异常类包括:
std::exception:表示通用异常的基类。std::logic_error:表示逻辑错误的异常类,例如无效参数、无效操作等。std::runtime_error:表示运行时错误的异常类,例如文件打开失败、内存分配失败等。std::vector、std::map 等)在某些操作失败时可能会引发异常,例如迭代器越界、内存分配失败等。throw 语句抛出,并在 try-catch 块中进行捕获和处理。通过捕获不同类型的异常,可以根据需要执行相应的异常处理操作。编译时异常(Compile-time Exceptions)是在编译代码期间检测到的异常。这些异常通常由编译器发现,指示了代码中的语法错误、类型错误或逻辑错误等问题。编译时异常会导致代码无法通过编译,因此在运行程序之前必须解决这些异常。例如,如果你在C++中使用了未声明的变量,编译器会报告一个编译时异常。
运行时异常(Runtime Exceptions)是在程序运行时发生的异常。这些异常通常是由于逻辑错误、算法错误、内存访问错误或运行时条件导致的问题。与编译时异常不同,运行时异常在代码编译和链接阶段不会被检测到。运行时异常可以通过异常处理机制来捕获和处理。如果未正确处理运行时异常,程序可能会终止或导致不可预测的行为。例如,在C++中,如果你访问一个空指针或者在数组中越界,就会引发运行时异常。
总结起来,编译时异常是在编译阶段检测到的错误,导致代码无法通过编译。而运行时异常是在程序运行时发生的错误,需要使用异常处理机制来捕获和处理。
使用内联函数的情况:
定义全局变量时使用static,意味着该变量的作用域只限于定义它的源文件中,其它源文件不能访问。既然这种定义方式出现在头文件中,那么可以很自然地推测:包含了该头文件的所有源文件中都定义了这些变量,即该头文件被包含了多少次,这些变量就定义了多少次。
在头文件中定义static变量会造成变量多次定义,造成内存空间的浪费,而且也不是真正的全局变量。应该避免使用这种定义方式。
可以控制变量和函数的生命周期、作用域和可访问性
静态变量:只能初始化一次,函数调用结束后保持其值,直到程序结束,存放在静态存储区
静态函数:直接通过类名调用,不需要创建类的实例,只能访问静态成员(静态变量和静态函数)
静态成员变量:所有实例共享, 而不是每个实例都有自己的副本 ,可以通过类名或对象来访问
文件作用域:只在当前源文件可见,不能被其他源文件访问
静态函数是类不是对象
当将一个成员声明为 static 时,它与普通成员变量有以下区别:
:: 来访问静态成员变量。例如,对于类 ClassName 的静态成员变量 staticMember,可以使用 ClassName::staticMember 进行访问。ClassName::staticMemberFunction()用于代码优化的不同机制
constexpr 是C++中的关键字,用于指示在编译时计算表达式的值,以提供编译时常量化和性能优化。它的主要用途包括:
编译器将不再允许这种隐式类型转换,要求使用显式的构造函数调用,防止可能引起混淆或错误的情况
int main(){ // 隐式类型转换 MyClass obj = 42;
// 编译器会调用 MyClass(int x) 构造函数
//显式类型转换 MyClass objExplicit = MyClass(42);
// 显式调用构造函数return0;
}
1)const声明常量,变量值不可改变
2)修饰函数参数,不可以修改传入参数
3)修饰成员函数,不可以修改对象的成员变量
4)修饰成员变量,表示变量在对象创建后不可被修改
5) 修饰指针const int*p: 常量指针,是一个指针,指向的数据不可改变,指针的指向可以改变
_int const *_p:指针常量,是一个常量,指向的数据可改变,指针的指向不可以改变
保持内存的可见性,编译期间不要优化,每次使用变量之前要重新读取,而不使用之前缓存的值
面向对象OOP的三大特性,提高代码的可维护性,可重用性和安全行
封装:将数据与操作数据的方法打包成类,私有化数据成员,提供公共接口与数据进行交互,
**继承:**派生类继承基类,扩展它们或者覆盖它们,有助于代码重用
多态:基类指针指向派生类对象,实现动态绑定,静态多态(重载),运行时多态(动态多态)
通过虚函数和继承实现,把需要实现多态的函数在基类声明为虚函数,子类中进行重写覆盖
虚函数表在子类和父类都有,子类覆盖或新增自己的虚函数,程序运行时会使用虚函数表来确定要调用的虚函数
1)每个包含虚函数的类都有一个虚函数表,虚函数表是一个数组,存储类中虚函数的地址
2) 对于每个类的实例,编译器在对象的内存布局中插入一个指向该类的虚函数表的指针,称为虚指针
3)对象调用虚函数的时候,实际通过虚指针找到虚函数表,根据函数在表中的索引来调用正确的虚函数,允许在运行期间确定要调用哪个实际函数
4)派生类没有虚函数会从基类继承虚函数表并覆盖
const和宏定义(#define)是用于定义常量的两种不同方式。
const是一种关键字,用于声明具有常量值的变量。它指定一个变量的值在初始化后不能被修改。const关键字可以应用于各种数据类型,包括基本类型(如整数、浮点数等)和自定义类型(如结构体、类等)。例如:
const int MAX_VALUE = 100;
const float PI = 3.14;
宏定义是一种预处理指令,用于在编译之前将标识符替换为指定的文本。它使用#define关键字定义,并且通常用于定义常量或简单的代码片段。宏定义不会进行类型检查,仅仅是简单的文本替换。例如:
#define MAX_VALUE 100
#define PI 3.14
const提供类型安全性,因为它可以应用于各种数据类型,并且在编译时进行类型检查。宏定义则没有类型安全性,它只是进行简单的文本替换,不进行类型检查。const具有块级作用域,它可以被限定在特定的作用域内。宏定义是全局的,它在整个程序中都可见。const在调试过程中提供更好的可读性,因为它们可以被调试器解析和显示。宏定义在调试过程中可能会引起困惑,因为它们只是文本替换,不会在调试器中显示真实的标识符。const通常用于声明单个常量,因此更容易理解和维护。const提供了更好的类型安全性、作用域控制、可读性和调试能力,而宏定义提供了更大的灵活性和代码简化的可能性。根据具体的使用场景和需求,选择合适的方式来定义常量。数据和代码捆绑在⼀起,避免外界⼲扰和不确定性访问。
封装,也就是把客观事物封装成抽象的类,并且类可以把⾃⼰的数据和⽅法只让可信的类或者对象操作,对不可信
的进⾏信息隐藏,例如:将公共的数据或⽅法使⽤public修饰,⽽不希望被访问的数据或⽅法采⽤private修饰。
public的变量和函数在类的内部外部都可以访问。
protected的变量和函数只能在类的内部和其派⽣类中访问。
private修饰的元素只能在类内访问


指相同对象收到不同消息或不同对象收到相同消息时产⽣不同的实现动作
实现多态有⼆种⽅式:覆盖(override),重载(overload)
覆盖(重写):是指⼦类重新定义⽗类的虚函数的做法
重载:是指允许存在多个同名函数,⽽这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许两者
都不同)。例如:基类是⼀个抽象对象——⼈,那教师、运动员也是⼈,⽽使⽤这个抽象对象既可以表示教师、也可以表示运动员。
在面向对象编程中,如果一个类有多个子类,类中的方法通常是被子类共享的,而不是每个子类都有一个副本。
当一个类定义了一个方法,该方法可以在该类的所有实例中使用。当子类继承父类时,子类会自动获得父类的所有方法和属性。子类可以使用继承的方法,并且还可以重写这些方法以适应子类的特定需求。
当子类重写一个方法时,子类的实例将使用子类中的方法实现,而不是父类中的方法。这称为方法重写。通过方法重写,子类可以覆盖父类中的方法,以实现不同的行为。
因此,如果一个类有很多子类,并且没有进行方法重写,那么这个类中的方法是被子类共享的,子类可以直接使用父类的方法。如果某个子类重写了父类的方法,那么该子类的实例将使用子类中的方法实现,而其他子类实例仍然使用父类的方法实现。
在这个示例中,Rectangle 和 Circle 类都继承了 Shape 类,并重写了父类的 calculate_area 函数以实现各自特定的面积计算逻辑。尽管它们是不同的子类,但它们共享了父类的方法,并根据自己的实现进行了方法重写
#include
class Shape {
public:
virtual double calculate_area() {
return 0;
}
};
class Rectangle : public Shape {
private:
double width;
double height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double calculate_area() override {
return width * height;
}
};
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double calculate_area() override {
return 3.14 * radius * radius;
}
};
int main() {
Rectangle rectangle(4, 5);
double rectangle_area = rectangle.calculate_area();
std::cout << "矩形的面积:" << rectangle_area << std::endl;
Circle circle(3);
double circle_area = circle.calculate_area();
std::cout << "圆的面积:" << circle_area << std::endl;
return 0;
}
this是一个指向当前对象的指针,它可以在类的成员函数内部使用。this指针指向调用该成员函数的对象本身,允许在函数内部访问对象的成员变量和成员函数。
当我们在类的成员函数中引用成员变量时,编译器会隐式地使用 this指针来访问对象的成员。例如,如果一个类有成员变量 value,在成员函数中使用 value 将被解释为 this->value。
this 指针的使用主要有以下几个方面:
1.** 解决命名冲突**:当成员变量与函数参数或局部变量名称相同时,可以使用 this 指针来显式指明访问的是成员变量而不是局部变量。
class MyClass {
private:
int value;
public:
void setValue(int value) {
this->value = value;
}
};
this 指针返回当前对象的引用,以便实现连续的操作。class MyClass {
private:
int value;
public:
MyClass& increment() {
++value;
return *this;
}
};
this 指针传递当前对象的地址。class MyClass {
public:
void process() {
someFunction(this);
}
};
需要注意的是,对于静态成员函数和非成员函数,它们没有 this 指针。静态成员函数是属于整个类而不是对象的,因此没有对特定对象的隐式访问。
总结起来,this 指针提供了对当前对象的隐式访问,允许在类的成员函数中访问对象的成员和操作对象本身。
移动构造函数(Move Constructor)和拷贝构造函数(Copy Constructor)是C++中用于对象初始化的特殊成员函数,它们在对象创建和复制时起到不同的作用。它们的区别如下:
语法:
ClassName(const ClassName& other)ClassName(ClassName&& other) noexcept作用对象:
资源转移:
性能:
右值引用:
在C++11及以后的标准中,通过使用右值引用和移动语义,可以明确指定移动构造函数。移动构造函数的出现主要是为了提高性能,在处理临时对象、函数返回值、容器元素插入等场景中特别有用。通过使用移动构造函数,可以避免不必要的内存分配和复制,提高程序的效率。
移动构造函数通常应该标记为noexcept,以指明它在执行时不会抛出异常。这样可以使得一些算法和容器在移动对象时进行优化,提高程序的稳定性和性能。
移动构造函数和拷贝构造函数在对象初始化和资源转移方面具有明显的区别。移动构造函数通过资源的移动而不是复制,提高了程序的性能。处理大型对象或需要频繁进行对象复制的情况下,使用移动构造函数可以显著提高性能。移动构造函数将资源的所有权从一个对象转移到另一个对象,而不进行昂贵的复制操作。这对于临时对象、函数返回值和容器元素等场景特别有用。
使用移动构造函数还可以避免不必要的内存分配和释放。当一个对象即将被销毁时,其资源可以被移动到另一个对象,从而避免了复制操作和额外的内存开销。这对于动态分配的资源(如堆内存)尤为重要,可以减少内存分配器的负担,提高内存管理的效率。
移动构造函数并不总是比拷贝构造函数更好。对于小型对象或不涉及资源转移的情况,拷贝构造函数可能更加高效。在某些情况下,编译器会自动优化对象的复制,将拷贝操作转换为移动操作,以提高效率。因此,选择移动构造函数还是拷贝构造函数要根据具体情况进行权衡和选择。
在C++11及以后的标准中,为了支持移动语义,还引入了移动赋值运算符(Move Assignment Operator)和移动语义相关的函数(如std::move和std::forward)。这些特性一起提供了更强大的资源管理和性能优化能力,使得移动操作更加方便和高效。
移动构造函数通过资源的移动而不是复制,提供了一种高效的对象初始化和资源转移方式。它在处理大型对象、避免不必要的内存分配和提高程序性能方面具有重要作用。然而,对于小型对象或不涉及资源转移的情况,拷贝构造函数仍然是常用的选择。
右值引用(Rvalue reference)是C++11引入的一种新类型的引用,用于标识右值(临时值)并允许对其进行特殊操作。右值引用与左值引用的主要区别在于它们可以绑定到不同类型的表达式,并且对绑定的对象的操作具有不同的语义。
&符号声明,如int&。&&符号声明,如int&&。std::forward可以实现参数的完美转发,即在函数中将传递给函数的参数以相同的方式转发给另一个函数。这对于泛型编程和函数包装器(如std::bind)非常有用,可以保留传递参数的值类别(左值还是右值)。void processValue(int& value) {
std::cout << "Lvalue reference: " << value << std::endl;
}
void processValue(int&& value) {
std::cout << "Rvalue reference: " << value << std::endl;
}
int main() {
int x = 5;
processValue(x); // 调用左值引用版本
processValue(10); // 调用右值引用版本
processValue(std::move(x)); // 调用右值引用版本
return 0;
}
根据传递给processValue函数的参数类型,将分别调用左值引用版本和右值引用版本。左值引用版本接受具名对象的引用,右值引用版本接受临时对象或即将被销毁的对象的引用。
3. 右值引用的特点:
&&符号进行声明。std::move函数可以将左值转换为右值引用,表示对其进行移动操作。std::forward函数,可以实现参数的完美转发。这对于泛型编程和函数包装器非常重要,可以保留传递参数的值类别(左值还是右值),确保参数以相同的方式传递给其他函数。右值引用的引入使得C++语言更加灵活和高效。它提供了一种新的引用类型,通过与移动语义和完美转发结合使用,可以实现更高效的资源管理和更灵活的参数传递。通过适当地使用右值引用,可以避免不必要的复制、提高程序的性能,并提供更强大的语言特性来支持现代C++编程。
在C++中,纯虚函数(Pure Virtual Function)是一种特殊的虚函数,它在基类中声明但没有提供具体的实现。纯虚函数通过在函数声明后面添加= 0来标识。
纯虚函数的主要作用是定义一个接口,强制派生类提供对应的实现。基类中的纯虚函数只是一个接口的规范,没有具体的实现,因此无法直接实例化基类对象。
基类中的至少一个纯虚函数会使得该基类成为抽象类(Abstract Class),抽象类不能被实例化。只有派生类实现了基类中的所有纯虚函数,才能实例化派生类对象。
纯虚函数的语法如下所示:
class Base {
public:
virtual void pureVirtualFunction() = 0;
// ...
};
需要注意的是,基类中的纯虚函数可以有其它成员函数和数据成员,而且基类也可以同时包含普通的虚函数。
派生类在继承了基类的纯虚函数后,必须实现这些函数,否则派生类也会成为抽象类。派生类可以选择在自己的定义中使用override关键字来明确表明它正在重写基类中的纯虚函数。
#include
class Base {
public:
virtual void pureVirtualFunction() = 0;
};
class Derived : public Base {
public:
void pureVirtualFunction() override {
std::cout << "Derived::pureVirtualFunction()" << std::endl;
}
};
int main() {
// Base base; // 错误,无法实例化抽象类
Derived derived;
derived.pureVirtualFunction(); // 输出:Derived::pureVirtualFunction()
Base* basePtr = &derived;
basePtr->pureVirtualFunction(); // 输出:Derived::pureVirtualFunction()
return 0;
}
总结:纯虚函数是一种在基类中声明但没有实现的虚函数,它用于定义接口并要求派生类提供对应的实现。纯虚函数使得基类成为抽象类,不能被实例化。派生类必须实现基类中的纯虚函数,才能实例化派生类对象。
sizeof是一个运算符,用于计算类型或表达式的字节大小。
sizeof运算符在编译时求值,而不是运行时。它返回的是静态的字节大小信息,不会随着程序的运行而改变。
C++的多态性是通过虚函数(Virtual Function)和基类指针/引用来实现的。多态性允许通过基类指针或引用调用派生类对象的成员函数,根据实际对象的类型动态地确定要调用的函数。
以下是多态的实现方式:
class Base {
public:
virtual void someFunction() {
// 虚函数的默认实现
}
// ...
};
class Derived : public Base {
public:
void someFunction() override {
// 派生类中对虚函数的实现
}
// ...
};
Base* ptr = new Derived(); // 创建派生类对象,并使用基类指针引用
ptr->someFunction(); // 调用虚函数,根据实际对象类型决定调用派生类的实现
delete ptr; // 注意要手动释放内存
在上述代码中,通过基类指针ptr调用someFunction()虚函数时,实际上会根据ptr指向的派生类对象类型,动态地调用派生类的实现。
关键点是在基类中将要被派生类重写的成员函数声明为virtual,这样就能实现运行时的多态性。通过基类指针或引用来操作派生类对象,可以根据实际对象的类型来决定调用哪个版本的虚函数。这使得多态性成为C++中的重要特性,能够实现灵活的对象行为和可扩展的代码设计。
虚函数是C++中用于实现多态性的一种机制。它允许通过基类指针或引用调用派生类对象的成员函数,并根据实际对象的类型动态地确定要调用的函数。
virtual来标识它为虚函数。虚函数可以有默认实现,也可以声明为纯虚函数(没有实际实现的函数,纯虚函数用= 0表示),使基类成为抽象类。class Base {
public:
virtual void someFunction() {
// 虚函数的默认实现
}
virtual void pureVirtualFunction() = 0;
// ...
};
Base* ptr = new Derived(); // 创建派生类对象,并使用基类指针引用
ptr->someFunction(); // 调用虚函数,根据实际对象类型决定调用派生类的实现
override关键字,确保正确覆盖基类的虚函数。class Derived : public Base {
public:
void someFunction() override {
// 派生类中对虚函数的实现
}
// ...
};
class Base {
public:
virtual ~Base() {
// 虚析构函数
}
// ...
};
虚函数的使用使得通过基类指针或引用操作派生类对象时,可以根据实际对象的类型来确定要调用的函数,实现多态性。这为面向对象的设计和代码组织提供了更大的灵活性和可扩展性。
在C++中,纯虚函数是没有实际实现的虚函数,通常用于定义基类的接口,要求派生类必须提供自己的实现。因此,纯虚函数通常在基类中没有具体的实现。
然而,在某些情况下,纯虚函数也可以在基类中实现。这样做的目的是为了提供一个默认的实现,但仍然要求派生类进行重写以满足自己的需求。在这种情况下,需要在基类的纯虚函数声明后面提供一个定义,使用= 0表示它是纯虚函数。
class Base {
public:
virtual void pureVirtualFunction() = 0; // 纯虚函数声明
virtual void someFunction() {
// 纯虚函数的默认实现
// 可以在基类中提供一个默认实现
// 但仍然要求派生类进行重写
// ...
}
// ...
};
void Base::someFunction() {
// 基类纯虚函数的默认实现
// 可以在这里提供一个具体的实现
// 但这个实现通常并不完整,只是为了提供一个默认行为
// ...
}
pureVirtualFunction()是基类中的纯虚函数声明,没有提供实际的实现。而someFunction()是基类中的虚函数,它被定义为提供纯虚函数的默认实现。这样做可以为纯虚函数提供一个通用的默认行为,但仍然要求派生类进行重写以满足自己的需求。
在基类中提供纯虚函数的默认实现并不常见,一般情况下我们更倾向于将纯虚函数留空,由派生类提供具体的实现。这样能够更好地遵循面向对象设计的原则,确保派生类必须实现纯虚函数。
在C++中,析构函数可以抛出异常,但这样做是不推荐的,并且可能会导致程序的不确定行为。
当析构函数抛出异常时,如果没有合适的异常处理机制,程序将会终止并调用 std::terminate 来终止程序的执行。这是因为在析构函数抛出异常后,对象的析构过程无法正常完成,资源可能没有正确释放,程序处于不一致的状态。
为了确保程序的稳定性和可预测性,通常建议在析构函数中不要抛出异常,或者在析构函数内部适当处理异常,以确保异常不会被传播到析构函数的调用方。
如果在析构函数中抛出异常,并且没有适当的异常处理机制,可能会导致以下问题:
1.** 资源泄漏**:如果析构函数抛出异常,可能导致对象持有的资源无法正确释放,从而产生资源泄漏。
2. 不确定状态:当析构函数抛出异常时,对象可能已经部分析构完成,但又无法完全析构,导致对象的状态处于不确定的状态。这可能会对程序的后续操作产生不可预测的影响。
3. 内存泄漏:如果在析构函数抛出异常后,对象的析构函数再次被调用(比如在异常处理中重新析构对象),可能会导致多次析构,从而产生内存泄漏或其他内存错误。
因此,为了保证程序的稳定性和可预测性,通常建议在析构函数中避免抛出异常,或者在析构函数内部适当处理异常,确保对象的析构过程能够正常完成。
纯虚函数是在基类中声明但没有提供实现的虚函数。纯虚函数通过在函数声明的末尾使用**= 0**来表示。基类中包含纯虚函数的类称为抽象类,而不能实例化抽象类的对象。
纯虚函数的主要目的是为了定义一个接口,强制要求派生类提供相应的实现。纯虚函数相当于一个占位符,用于表示派生类必须重写该函数,以便使派生类变为可实例化的具体类。通过将函数声明为纯虚函数,基类可以定义一组接口规范,而具体的实现由派生类提供。
纯虚函数的使用有以下几个主要原因:
编译器对类的布局和对齐的具体实现会因编译器、编译选项和目标平台等因素而有所不同。类的实际大小可能会受到编译器和环境的影响。可以使用sizeof运算符来获取类的大小.
1)std::array是封装了固定大小的容器,其结合了C风格数组的性能和C++可访问性容器的优点,如支持容器大小,可赋值,随机访问等。
2)std::forward_list单向链表,节省了内存,同时又有比list更好的运行时性能
forward_list只有一个直接访问元素的接口:front()
3)**std::unordered_map,**哈希map理论上查找效率为O(1)。但在存储效率上,哈希map需要增加哈希表的内存开销。适合需要高效率查询的情况
**4)std::unordered_set,**基于哈希表的无序集合,具有快速查找、删除和添加等优点,插入时不会自动排序
emplace_back() 和 push_abck() 的区别是:push_back() 在向 vector 尾部添加一个元素时,会创建一个临时对象,然后再将这个临时对象移动或拷贝到 vector 中(如果是拷贝的话,事后会自动销毁先前创建的这个临时元素);而 emplace_back() 在实现时,则是直接在 vector 尾部创建这个元素,省去了移动或者拷贝元素的过程。
拷贝构造函数是一种特殊的构造函数,用于创建一个对象时,通过复制另一个同类对象的数据来初始化新对象。它的声明形式通常是类名(const 类名& other),其中"other"是要被复制的对象。
拷贝构造函数主要用在以下场景:
需要注意的是,如果没有显式定义拷贝构造函数,编译器会自动生成一个默认的拷贝构造函数,该函数按位复制对象的数据成员。然而,对于包含指针或资源管理的类,这样的默认实现可能会导致浅拷贝问题,因此可能需要显式定义拷贝构造函数来执行深拷贝操作,确保正确的对象复制和资源管理。
值传递:
形参是实参的拷贝,改变形参的值并不会影响外部实参的值。从被调用函数的角度来说,值传递是单向的(实参->形参),参数的值只能传入,不能传出。当函数内部需要修改参数,并且不希望这个改变影响调用者时,采用值传递。
指针传递: 指针传递参数本质上是值传递的方式,它所传递的是一个地址值
形参为指向实参地址的指针,当对形参的指向操作时,就相当于对实参本身进行的操作
引用传递:
形参相当于是实参的“别名”,对形参的操作其实就是对实参的操作,在引用传递过程中,被调函数的形式参数虽然也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量。正因为如此,被调函数对形参做的任何操作都影响了主调函数中的实参变量。
引用传递和指针传递有什么区别吗?
(1)引用被创建的同时必须被初始化(指针则可以在任何时候被初始化)。
(2)不能有NULL引用,引用必须与合法的存储单元关联(指针则可以是NULL)
(3)一旦引用被初始化,就不能改变引用的关系(指针则可以随时改变所指的对象)
指针传递和引用传递一般适用于:
函数内部修改参数并且希望改动影响调用者。对比指针/引用传递可以将改变由形参“传给”实参(实际上就是直接在实参的内存上修改,不像值传递将实参的值拷贝到另外的内存地址中才修改)。
另外一种用法是:当一个函数实际需要返回多个值,而只能显式返回一个值时,可以将另外需要返回的变量以指针/引用传递给函数,这样在函数内部修改并且返回后,调用者可以拿到被修改过后的变量,也相当于一个隐式的返回值传递吧。
#include
using namespace std;
//值传递
void change1(int n){
cout<<"值传递--函数操作地址"<<&n<<endl; //显示的是拷贝的地址而不是源地址
n++;
}
//引用传递
void change2(int & n){
cout<<"引用传递--函数操作地址"<<&n<<endl;
n++;
}
//指针传递
void change3(int *n){
cout<<"指针传递--函数操作地址 "<<n<<endl;
*n=*n+1;
}
int main(){
int n=10;
cout<<"实参的地址"<<&n<<endl;
change1(n);
cout<<"after change1() n="<<n<<endl;
change2(n);
cout<<"after change2() n="<<n<<endl;
change3(&n);
cout<<"after change3() n="<<n<<endl;
return true;
}

std::ifstream file;
file.open("filename.txt", std::ios::in);
if (file.is_open()) {
// 文件成功打开
} else {
// 文件打开失败
}
std::string line;
std::getline(file, line);
如果你使用输出流对象(如 ofstream 或fstream),你可以使用 <<运算符或 write 方法来向文件中写入数据。
file << "Hello, world!";
close 方法关闭文件流。file.close();
以上是使用C++中的"open"函数打开文件的一般流程。请注意,在进行文件操作时,始终应该检查操作是否成功,并且在不需要文件流时及时关闭文件。
strlen:
strlen 是 C 语言中的函数,用于计算字符串的长度(不包括字符串末尾的’\0’)。
仅适用于以 null 字符结尾的字符串,即 C-style 字符串。
返回值类型为 size_t。
length/size:
length 和 size 都是 C++ 中 string 类型的成员函数,用于返回字符串的长度。
可以适用于任何字符串类型,包括 std::string 类型和 C-style 字符串类型。
返回值类型为 size_t。
在 C++ 中,std::string 的 length 和 size 成员函数不包含字符串末尾的 null 字符,因此它们返回的值是字符串的实际长度,不包括 null 字符。
sizeof:(包括字符串末尾的’\0’)
sizeof 是 C 和 C++ 中的操作符,用于返回其操作数的大小(以字节为单位)。
对于 C-style 字符串,sizeof 返回的是字符串数组的大小**,包括字符串末尾的 null 字符**,而不是字符串的长度。
对于 std::string 类型,sizeof 返回的是字符串对象本身的大小,而不是字符串的长度。
返回值类型为 size_t。
(1)当用类的一个对象去初始化该类的另一个对象时,系统会自动调用拷贝构造函数;
(2)将一个对象作为实参传递给一个非引用类型的形参,系统会自动调用拷贝构造函数;
(3)从一个返回类为非引用的函数返回一个对象时,系统会自动调用拷贝构造函数;
(4)用花括号列表初始化一个数组的元素时,系统会自动调用拷贝构造函数
将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存的搬迁或者拷贝

没有参数的构造函数,将告诉编译器生成一个默认构造函数,它将初始化 ptr 和 ref_count 为默认值(通常为 nullptr 和 0
左值引用:给变量取别名,可以减少一层拷贝,左值是一个表示数据的表达式(变量名或解引用的指针)
右值引用:如字面常量、表达式返回值、函数返回值(不能是左值引用返回)等待,右值不能取地址
要区分左值与右值,只需要记住一个标准:“左值可以取地址,右值不能被取地址”。这就是左值和右值的本质区别。
因为左值一般是被放在地址空间中的,有明确的存储地址;而右值一般可能是计算中产生的中间值,也可能是被保存在寄存器上的一些值,总的来讲就是,右值并没有被保存在地址空间中,也就无法取地址。
C/C++从右到左
在不使用完美转发的情况下,函数参数的传递方式决定了形参的值类别,因此会导致值类别丢失
void byValue(int x) {
// x 是局部变量,因此它是左值
}
void byReference(int& x) {
// x 是左值引用,它的值类别与传入参数相同
}
int main() {
int a = 42;
byValue(a); // 传入左值,x 是左值
byValue(123); // 传入右值,x 仍然是左值
int b = 56;
byReference(b); // 传入左值,x 是左值引用
byReference(789); // 传入右值,x 仍然是左值引用
return 0;
}
允许函数将其参数传递给另一个函数,同时保留原始参数的值类别(左值或右值)
使用完美转发可以提高代码的可复用性和性能,因为它允许函数传递参数给其他函数,而不会丢失参数的信息。这在实现通用函数、容器和类模板等情况下非常有用
template <typename T>
void forwarder(T&& arg) {
target(std::forward<T>(arg)); // 使用 std::forward 进行完美转发
}
void target(int& x) {
std::cout << "L-value reference: " << x << std::endl;
}
void target(int&& x) {
std::cout << "R-value reference: " << x << std::endl;
}
int main() {
int x = 42;
forwarder(x); // 调用 L-value reference 版本
forwarder(123); // 调用 R-value reference 版本
return 0;
}
当引用与其他引用相遇或与引用与非引用相遇时,编译器会将它们合并成一个引用的规则
template <typename T>
void foo(T&& t) {
// 引用折叠:根据参数的值类别来选择合适的操作
// 如果传入左值,t 是左值引用;如果传入右值,t 是右值引用
t = 100; // 修改 t 对应的对象
}
int main() {
int x = 42;
foo(x); // x 是左值,t 是左值引用
foo(123); // 123 是右值,t 是右值引用
return 0;
}
正向,反向,双向
#include
template
class SimpleForwardIterator {
public:
// 构造函数
SimpleForwardIterator(T* ptr) : ptr_(ptr) {}
// 解引用操作符
T& operator*() const {
return *ptr_;
}
// 前进操作
SimpleForwardIterator& operator++() {
++ptr_;
return *this;
}
// 后置递增操作
SimpleForwardIterator operator++(int) {
SimpleForwardIterator temp = *this;
++ptr_;
return temp;
}
// 比较操作符
bool operator!=(const SimpleForwardIterator& other) const {
return ptr_ != other.ptr_;
}
private:
T* ptr_;
};
int main() {
int arr[] = {1, 2, 3, 4, 5};
// 创建一个简单的前向迭代器
SimpleForwardIterator begin(arr);
SimpleForwardIterator end(arr + 5);
// 使用迭代器遍历数组
for (SimpleForwardIterator it = begin; it != end; ++it) {
std::cout << *it << " ";
}
return 0;
}
写一个迭代器
unordered_map myMap;
//使用迭代器遍历无序映射
for (unordered_map::iterator it = myMap.begin(); it != myMap.end(); ++it)
{ std::cout << "Key: " << it->first << ", Value: " << it->second << std::endl; }
1)插入和删除操作
2)删除容器元素
3)容器的重新分配
4)end()迭代器的失效
1)使用一个对象去初始化同类型的另一个对象
2)以值传递的方式传递对象给函数
3)从函数返回一个对象
4)初始化容器元素,比如vector里面的push_back,emplace_back,都会调用对象的拷贝构造函数,但push_back得构造再拷贝,emplace_back是再容器中直接构造,如果enplace_back得时候容器中得内存正好被占用,会分配一个更大的内存块,然后把旧元素都拷贝过来
可以避免程序员忘记手动释放内存,防止内存泄漏
早期C++98是auto_ptr,但是它赋值或拷贝控制所有权会转移
C++11里面引入unique_ptr,是独占式的,任意时刻只有一个unique_ptr可以指向相同的动态内存
禁用了拷贝构造函数,但不可以进行资源所有权转移了,非要转移得用move语义
#includeintmain(){
std::unique_ptr uniquePtr = std::make_unique(42);
// 当 uniquePtr 超出作用域时,关联的内存将被自动释放return0;
}
后面引入了shared_ptr,是共享式的,可以多个智能指针指向同一块内存,引用计数器来跟踪有多少个智能指针共享相同的对象,当最后一个共享指针被销毁的时候,关联对象才会被销毁
#include
intmain(){
std::shared_ptr sharedPtr1 = std::make_shared(42);
std::shared_ptr sharedPtr2 = sharedPtr1;
// 共享所有权// 当 sharedPtr1 和 sharedPtr2 超出作用域时,关联的内存将被自动释放return0;
}
但会产生循环依赖的问题
在后面就引入了week_ptr,可以解决循环依赖的问题
template<typename T>
class MyUniquePtr{
public:
//构造
MyUniquePtr(T* ptr):ptr_=ptr{}
//禁用拷贝构造和拷贝赋值
MyUniquePtr(const MyUniquePtr&)=delete;
MyUniquePtr& oeprator=(const MyUniquePtr&)=delete;
//移动构造
MyUniquePtr(MyUniquePtr&&other){
ptr_=other.ptr_;
other.ptr_=nullptr;
}
//移动赋值
MyUniquePtr& oeprator=(MyUniquePtr&&other){
if(this!=&other){
delete ptr_;
ptr_=other.ptr_;
other.ptr_=nullptr;
}
return *this;
}
//获取指针
T*get(){
return ptr_;
}
//解引用
T& operator*()const{
return *ptr_;
}
//成员访问
T* operator->()const{
return ptr_;
}
//析构
~MyUniquePtr(){
delete ptr_;
}
private:
T* ptr_;
};
int main() {
// 使用 MyUniquePtr 演示
MyUniquePtr<int> uniquePtr(new int(42));
std::cout << "Value: " << *uniquePtr << std::endl;
// 移动构造函数
MyUniquePtr<int> anotherPtr = std::move(uniquePtr);
std::cout << "Value in anotherPtr: " << *anotherPtr << std::endl;
// 移动赋值运算符
MyUniquePtr<int> yetAnotherPtr(new int(99));
yetAnotherPtr = std::move(anotherPtr);
std::cout << "Value in yetAnotherPtr: " << *yetAnotherPtr << std::endl;
return 0;
}
#include
template <typename T>
class SmartPtr {
public:
// 构造函数
explicit SmartPtr(T* ptr = nullptr) : ptr_(ptr), ref_count_(new size_t(1)) {}
// 拷贝构造函数
SmartPtr(const SmartPtr& other) : ptr_(other.ptr_), ref_count_(other.ref_count_) {
++(*ref_count_);
}
// 拷贝赋值运算符
SmartPtr& operator=(const SmartPtr& other) {
if (this != &other) {
// 减少旧引用计数
decreaseRef();
// 复制新指针和引用计数
ptr_ = other.ptr_;
ref_count_ = other.ref_count_;
// 增加新引用计数
++(*ref_count_);
}
return *this;
}
//移动构造
SmartPtr(SmartPtr&& other) noexcept :ptr_(nullptr), ref_count_(nullptr) {
swap(*this, other);
}
//移动赋值
SmartPtr& operator=(SmartPtr other) {
swap(*this, other);
return *this;
}
// 析构函数
~SmartPtr() {
// 减少引用计数
decreaseRef();
}
// 重载解引用运算符
T& operator*() const {
return *ptr_;
}
// 重载箭头运算符
T* operator->() const {
return ptr_;
}
friend void swap(SmartPtr& a, SmartPtr& b) {
std::swap(a.ptr_, b.ptr_);
std::swap(a.ref_count_, b.ref_count_);
}
private:
// 减少引用计数,可能释放内存
void decreaseRef() {
if (ref_count_ != nullptr && --(*ref_count_) == 0) {
delete ptr_;
delete ref_count_;
}
}
private:
T* ptr_; // 指向被管理对象的指针
size_t* ref_count_; // 引用计数
};
int main() {
SmartPtr<int> ptr1(new int(42));
SmartPtr<int> ptr2 = ptr1; // 共享所有权
std::cout << *ptr1 << std::endl; // 42
std::cout << *ptr2 << std::endl; // 42
*ptr1 = 10; // 修改共享对象
std::cout << *ptr1 << std::endl; // 10
std::cout << *ptr2 << std::endl; // 10
return 0; // SmartPtr 在此处自动释放内存
}
1.** 所有权管理:**
shared_ptr 内部包含一个指针,用于指向动态分配的对象。这个指针通常是通过 new 运算符分配内存得到的。指针管理部分负责在适当的时候释放对象的内存。shared_ptr 通过引用计数来跟踪有多少个 shared_ptr 实例共享同一个对象。引用计数通常以整数形式存在,并存储在与所指对象相关联的控制块(control block)中。shared_ptr 内部的一个数据结构,用于存储引用计数和其他相关信息。控制块通常是动态分配的,并与所指对象共享。shared_ptr 时,它会与一个控制块关联,并将指针指向所管理的对象。控制块中的引用计数会被初始化为1。当创建其他 shared_ptr 实例并与同一个对象关联时,引用计数会递增。当 shared_ptr 被销毁或重置时,引用计数会递减。当引用计数为0时,表示没有 shared_ptr 实例与该对象关联,此时控制块会负责释放对象的内存。shared_ptr 的默认实现是线程安全的,它使用原子操作来确保引用计数的并发访问安全。这样可以在多线程环境下安全地共享对象。shared_ptr 对象时,引用计数会初始化为1。当将一个 shared_ptr 对象赋值给另一个 shared_ptr 对象或进行拷贝构造时,引用计数会增加。std::shared_ptr<int> ptr1(new int(5)); // 引用计数为1
std::shared_ptr<int> ptr2 = ptr1; // 引用计数加1
reset 函数或赋值操作符 = 将 shared_ptr 重新指向新的对象,会导致引用计数的变化。旧的对象引用计数减少,新的对象引用计数增加。std::shared_ptr<int> ptr(new int(5)); // 引用计数为1
ptr.reset(new int(10)); // 引用计数减少到0,释放旧对象;引用计数变为1,指向新对象
shared_ptr 超出作用域或通过 reset 函数将其置空时,引用计数减少。当引用计数变为0时,内部资源会被释放,即对象会被删除。std::shared_ptr<int> ptr(new int(5)); // 引用计数为1
// 引用计数减少到0,内部对象被删除
在拷贝构造函数中,新创建的 shared_ptr 引用计数会增加;
在赋值操作符中,目标 shared_ptr 的引用计数会增加,而源 shared_ptr(如果有)的引用计数会减少
unique_ptr 是一种独占所有权的智能指针,一个 unique_ptr 对象可以拥有对一个动态分配对象的唯一所有权。当 unique_ptr 被销毁或重置时,它所管理的对象会被析构。
如果一个 unique_ptr 指向两个不同的对象,例如通过移动语义或者重新分配 unique_ptr 的所有权,那么当 unique_ptr 被销毁或重置时,只会调用其中一个对象的析构函数。
具体来说,当 unique_ptr 被销毁或重置时,它会检查当前是否存在一个有效的指针(即指向一个对象)。如果存在有效的指针,那么会调用该对象的析构函数来销毁它。对于另一个对象,由于它的所有权已经转移到其他 unique_ptr 或已被释放,所以不会调用其析构函数。
需要注意的是,在同一个 unique_ptr 对象上进行多次重置会导致其之前指向的对象被销毁多次,这是一种未定义行为。确保在重置 unique_ptr 之前,将其指向的对象正确释放或移交给其他 unique_ptr 或智能指针。
总结:unique_ptr 只能拥有对一个对象的唯一所有权,当 unique_ptr 被销毁或重置时,只会调用其中一个对象的析构函数。
shared_ptr 是一种共享所有权的智能指针,它可以与其他 shared_ptr 共享对同一个动态分配对象的所有权。当最后一个 shared_ptr 对象被销毁或重置时,才会调用所管理对象的析构函数。
如果一个 shared_ptr 指向两个不同的对象,即多个 shared_ptr 共享对这两个对象的所有权,那么当最后一个 shared_ptr 被销毁或重置时,才会调用这两个对象的析构函数。
shared_ptr 内部通过引用计数来跟踪共享的对象,并在引用计数为0时释放对象的内存。每个 shared_ptr 对象都有一个关联的控制块(control block),其中包含引用计数等信息。当一个新的 shared_ptr 对象与现有的对象关联时,引用计数会增加。当一个 shared_ptr 对象被销毁或重置时,引用计数会减少。只有当引用计数降为0时,才会调用对象的析构函数。
因此,只有当最后一个 shared_ptr 对象与这两个对象的所有权解除关联时,也就是引用计数降为0时,才会调用这两个对象的析构函数。其他 shared_ptr 对象的销毁或重置不会触发析构函数的调用,只会减少引用计数。
需要注意的是,使用 shared_ptr 时要注意避免循环引用,即两个或多个对象相互持有对方的 shared_ptr,这会导致对象无法释放,内存泄漏的问题。可以使用 weak_ptr 打破循环引用,以便正确释放对象。
unique_ptr 可以指向在堆区(动态分配)的对象,而不能直接指向栈区(自动分配)的对象。
在函数内部创建一个 unique_ptr 并使用 new 运算符来分配内存,那么它将指向堆区的对象。这是因为 unique_ptr 是专门用于管理动态分配的对象的智能指针。在函数执行完毕或 unique_ptr 被销毁时,它会自动释放所管理的堆区对象。
例如,下面的示例演示了在函数内部使用 unique_ptr 来管理动态分配的对象:
#include
void foo() {
std::unique_ptr<int> ptr(new int(10)); // 指向堆区的对象
// 使用 ptr
// ...
} // 在函数结束时,ptr 被销毁,堆区对象也会被自动释放
unique_ptr ptr 在函数内部被创建,并通过 new 运算符在堆区分配了一个整型对象。当函数 foo 结束时,ptr 被销毁,它的析构函数会自动释放堆区对象。
如果你尝试将 unique_ptr 直接指向栈区对象,会导致编译错误。因为 unique_ptr 的析构函数会尝试删除(delete)指向的对象,而栈区对象在函数结束时会自动被销毁,不能通过 delete 来释放。
因此,一般情况下,unique_ptr 用于管理在堆区分配的对象,而不是栈区的对象。如果需要管理栈区对象,可以使用原生指针或其他适当的智能指针(如 std::shared_ptr)。
shared_ptr 可以指向在堆区(动态分配)的对象,也可以指向栈区(自动分配)的对象。它并不关心对象是在堆区还是栈区分配的,而是关心对对象的所有权管理。
当你在函数内部创建一个 shared_ptr 并将其指向堆区对象时,它将管理堆区对象的所有权。当最后一个持有该对象的 shared_ptr 被销毁时,堆区对象将被自动释放。
当你在函数内部创建一个 shared_ptr 并将其指向栈区对象时,它同样可以管理栈区对象的所有权。但需要注意的是,当函数结束时,栈区对象会被自动销毁,而 shared_ptr 的析构函数会尝试释放对象,这可能导致未定义行为。因此,为了避免这种情况,如果你使用 shared_ptr 管理栈区对象,需要在 shared_ptr 被销毁之前手动释放或者重置它。
#include
void foo() {
// 在堆区分配对象
std::shared_ptr<int> ptr1(new int(10)); // 指向堆区的对象
// 在栈区分配对象
int value = 20;
std::shared_ptr<int> ptr2(&value, [](int*) { }); // 指向栈区的对象,自定义删除器为空
// 使用 ptr1 和 ptr2
// ...
} // 在函数结束时,ptr1 和 ptr2 被销毁,堆区对象会自动释放,栈区对象不会被释放
在上述示例中,ptr1 是指向堆区分配的整型对象的 shared_ptr。ptr2 是指向栈区的整型对象的 shared_ptr,使用了一个自定义的删除器,但这里删除器为空,因为栈区对象不需要被释放。
需要注意的是,当你使用 shared_ptr 管理栈区对象时,确保在 shared_ptr 对象被销毁之前,不要访问已经销毁的栈区对象。并且要避免在多个 shared_ptr 之间共享对栈区对象的所有权,以防止悬空指针的问题。一般情况下,shared_ptr 更适合用于管理动态分配的堆区对象。
智能指针是C++中用于管理动态分配内存的工具,它们提供了自动化的内存管理,可以避免常见的内存泄漏和悬挂指针等问题。以下是几种常见的智能指针及其作用:
智能指针的释放时机取决于其作用域和所有权的转移。
1)帮C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏!
2)new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露!
3)使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会!
4)在动态内存管理中,使用智能指针可以避免手动管理内存的麻烦和出错风险。
分配的动态内存都交由有生命周期的对象来处理,那么在对象过期时,让它的析构函数删除指向的内存
auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new 获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete 来释放内存!
因为其里面重载了 * 和 -> 运算符, * 返回普通对象,而 -> 返回指针对象
1)复制或者赋值都会改变资源的所有权
2)在STL容器中使用auto_ptr存在着重大风险,因为容器内的元素必须支持可复制和可赋值
3)不支持对象数组的内存管理
C++11用更严谨的unique_ptr 取代了auto_ptr!
unique_ptr 和 auto_ptr用法几乎一样,除了一些特殊。
vec[0]= vec[1]; /* 不允许直接赋值 */
vec[0]= std::move(vec[1]);// 需要使用move修饰,使得程序员知道后果
shared_ptr作为被管控的对象的成员时,小心因循环引用造成无法释放资源!
要注意避免对象交叉使用智能指针的情况! 否则会导致内存泄露!
解决方法:使用weak_ptr弱指针。
weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。 同时weak_ptr 没有重载*和->但可以使用 lock 获得一个可用的 shared_ptr 对象。
在调用u.release()时是不会释放u所指的内存的,这时返回值就是对这块内存的唯一索引,如果没有使用这个返回值释放内存或是保存起来,这块内存就泄漏了.
如果我们主动释放掉get 函数获得的指针,那么智能 指针内部的指针就变成野指针了,析构时造成重复释放,带来严重后果!
捕获当前对象的引用。这允许 Lambda 表达式在其函数体中访问当前对象的成员变量和成员函数
修改外部作用域的变量要加mutable关键字
Lambda函数,是一种在编程语言中用于表示内联函数或匿名函数的特殊语法结构。它允许我们定义一个临时的、无需命名的函数,通常用于需要在某个特定的上下文中定义和使用函数的场景。
在C++中,Lambda函数是通过Lambda表达式来创建的。Lambda表达式具有以下一般形式:
//capture list是用于捕获变量的列表,
//parameters是函数参数
//return_type是返回类型
//函数体是实际的函数代码
[capture list](parameters) -> return_type {
// 函数体
}
#include
int main() {
int x = 10;
int y = 20;
// Lambda函数示例
auto sum = [x, &y](int a, int b) -> int {
y = 30; // 修改外部变量y
return a + b + x + y;
};
int result = sum(5, 7);
std::cout << "Result: " << result << std::endl; // 输出:Result: 52
return 0;
}
上述示例中的Lambda函数通过捕获变量x和引用捕获变量y,计算了两个参数的和,并访问了外部的x和y变量。Lambda函数的返回类型被指定为int。
Lambda函数是一种方便的方式来定义匿名的、临时的函数,提供了更灵活和简洁的代码编写方式,特别适用于需要在特定上下文中定义和使用函数的场景。
匿名函数则是在需要一个函数,但是又不想费力去命名一个函数的情况下去使用的。
[ caputrue ] ( params ) opt -> ret { body; };
vector<int>vec;
sort(vec.begin(), vec.end(),[](int a,int b){return a > b;});
模板(Template)是C++中的一种强大的泛型编程工具,它允许编写通用的代码,能够适用于多种不同的数据类型。模板通过参数化类型和参数化值来定义通用的函数、类、数据结构等。
模板可以分为函数模板和类模板两种形式。
1**. 函数模板:**
template开始,后面跟着模板参数列表和函数的定义。 template <typename T>
void swapValues(T& a, T& b) {
T temp = a;
a = b;
b = temp;
}
2. 类模板:
template开始,后面跟着模板参数列表和类的定义。template <typename T>
class Stack {
private:
std::vector<T> data;
public:
void push(const T& item) {
data.push_back(item);
}
T pop() {
T item = data.back();
data.pop_back();
return item;
}
};
使用模板的好处包括:
模板的底层实现是通过编译器在编译时进行代码生成和实例化。编译器根据模板定义和使用的上下文,在编译阶段将模板代码生成具体的代码。
具体而言,编译器在遇到模板定义时,并不会立即生成代码,而是将模板代码存储在编译器的符号表中,并对其进行语法和语义的检查。当编译器遇到模板的实例化(即模板被使用)时,它会根据实际的模板参数,生成对应的具体代码。
实例化模板的过程包括两个主要的步骤:
C++模板是一种通用编程机制,允许您编写可以处理多种数据类型的代码,而无需为每种数据类型都编写特定的代码。模板是C++中强大而灵活的功能,主要包括类模板和函数模板。
//类模板的比较函数
template
class Compare{
public:
//比较函数
T max(T a,T b){
return (a>b)?a:b;
}
};
//函数模板
template
T max(T a,T b){
return (a>b)?a:b;
}
:::info
C++模板是一种强大的编程工具,但它们也有优点和缺点,取决于如何使用和实现。以下是模板的一些主要优点和缺点:
优点
缺点:
#include
using namespace std;
//函数模板
template<typename type1,typename type2>
type1 Max(type1 a,type2 b){
return a > b ? a : b;
}
int main(){
cout<<"Max = "<<Max(5.5,4.5)<<endl;
return 0;
}