main函数之前
设置栈指针
初始化静态变量和全局变量(即.data内容);将未初始化的全局变量赋值:short、int、long初始化为0,bool初始化为false,指针指向NULL(即.bss内容)
执行全局对象的构造函数
将main函数的参数argc和argv传递给main函数
__attribute__((constructor))
main函数之后
执行全局对象的析构函数
用atexit注册一个函数,做善后工作
__attribute__((destructor))
补充知识:
__attribute__
可以设置函数属性(Function Attribute )、变量属性(Variable Attribute )和类型属性(Type Attribute ),用于初始化一些在程序中使用的数据
编译过程分为三个过程:编译(编译预处理、编译、优化),汇编,链接。
编译预处理:处理以 # 开头指令;
编译、优化:将.cpp源码翻译成.s汇编代码;包括词法分析:编译器会读取源代码,将源代码分解成若干个单词,并将每个单词转换成词法单元,例如关键字、标识符、操作符等。语法分析:编译器会对词法分析得到的词法单元进行解析和分析,生成语法树,并将其转换为中间代码。语义分析:编译器会对中间代码进行分析,检查代码是否符合语言的语法规范,包括数据类型是否匹配、符号是否定义、函数是否调用正确等。代码优化:编译器会对生成的中间代码进行分析和优化,以提高代码执行效率,例如去除无用代码、利用CPU指令优化代码等。代码生成:编译器会根据中间代码生成目标代码,如汇编语言。
汇编:将.s汇编代码翻译成.o机器指令;
链接:由于.cpp文件中的函数可能引用了另一个.cpp文件中定义的符号或者函数,链接的目的就是将文件对应的其他目标文件连接成一个整体,从而生成可执行的程序.exe。(链接阶段可以发现被调用的函数未定义)
二者的优缺点:
C++ 内存分区:栈、堆、全局/静态存储区、常量存储区、代码区。
管理方式:
内存管理机制:
哪些操作可能会导致栈溢出?(1)递归调用:当一个函数多次递归调用自身时,每次调用都会在栈上分配一段新的内存空间,导致栈空间被耗尽。(2)大量函数调用:如果频繁地调用函数,每次函数调用都需要在栈上分配一部分内存用于保存函数的信息,过多的函数调用导致栈溢出。(3)局部变量过多或过大:如果在函数内部定义了过多的局部变量,或者某个局部变量占用的内存过大,超过栈空间的大小可能导致溢出。
空间大小:
为什么栈空间比堆空间小?因为栈空间主要存储的是函数的局部变量、函数参数及返回值地址,一个程序中这些信息所占内存较小,且栈空间用户是无法进行操作,是由编译器和操作系统决定。
碎片问题:
生长方向:
分配效率(堆快还是栈快):
全局变量、局部变量、静态全局变量、静态局部变量的区别:
(补充知识:C++作用域可分为 6 种:全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域。)
作用域:
分配内存空间:
如果在头文件中定义全局变量,当该头文件被多个文件 include 时,该头文件中的全局变量就会被定义多次,导致重复定义,因此不能在头文件中定义全局变量。
什么是内存对齐?内存对齐的原则?为什么要进行内存对齐,有什么优点?
内存对齐:数据在计算机存储时按照特定规则对齐的过程,通常以特定字节大小的块进行对齐。
结构体内存对齐的原则(注:对齐基数可以通过#pragma pack ()
设置,现在机器默认是 8):
进行内存对齐的原因:(主要是硬件设备方面的问题)
内存对齐的优点:
并非指内存从物理上消失,而是由于疏忽或错误导致的程序不能释放已经不再使用的内存。常指堆内存泄漏,使用 malloc、new 申请内存空间时,使用完后要free或 delete释放内存,否则会产生内存泄漏。指针重新赋值也容易导致内存泄漏,如下:
char *p = (char *)malloc(10);
char *p1 = (char *)malloc(10);
p = np;
开始时,指针 p 和 p1 分别指向一块内存空间,但指针 p 被重新赋值,导致 p 初始时指向的那块内存空间无法找到,从而发生了内存泄漏。
智能指针是为了解决动态内存分配时带来的内存泄漏以及多次释放同一块内存空间而提出的,封装在了memory头文件中。
C++11 中智能指针包括以下三种:
借助 std::move() 可以实现将一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,其目的是实现所有权的转移。
// A 作为一个类
std::unique_ptr<A> ptr1(new A());
std::unique_ptr<A> ptr2 = std::move(ptr1);
智能指针可能出现的问题:循环引用
如果定义了两个类CA、CB,在两个类中分别定义另一个类的对象的共享指针。程序结束后,两个指针相互指向对方的内存空间,出现了循环引用,导致内存无法释放,造成内存泄漏。
循环引用的解决方法:
将其中一个共享指针定义为弱指针,weak_ptr 对被 shared_ptr 管理的对象存在弱引用,weak_ptr 用来表达临时所有权的概念,不增加引用计数。当某个对象只有存在时才需要被访问,需要获得所有权时将其转化为 shared_ptr。
字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序
网络字节顺序采用大端排序方式,往往将小端IP转为大端IP,才能进行网络传输
法1:使用强制类型转换(由于int和char的长度不同,借助int型转换成char型,只会留下内存低地址的部分)
#include
using namespace std;
int main()
{
int a = 0x1234;
//由于int和char的长度不同,借助int型转换成char型,只会留下低地址的部分
char c = (char)(a);
if (c == 0x12)
cout << "big endian" << endl;
else if(c == 0x34)
cout << "little endian" << endl;
}
法2:使用union联合体
#include
using namespace std;
//union联合体的重叠式存储,endian联合体占用内存的空间为每个成员字节长度的最大值
union endian
{
int a;
char ch;
};
int main()
{
endian value;
value.a = 0x1234;
//a和ch共用4字节的内存空间
if (value.ch == 0x12)
cout << "big endian"<
1. auto自动类型推导
编译器会在编译期间通过初始值推导出变量的类型,即通过auto定义的变量必须有初始值。
2. decltype 声明类型
和 auto 的功能一样,都是在编译时期进行自动类型推导。如果希望从表达式中推断出要定义的变量的类型,但是不想用该表达式的值初始化变量就用decltype,decltype 作用是返回操作数的数据类型,定义变量的时候可初始化也可不初始化。
auto var = val1 + val2;
decltype(val1 + val2) var1 = 0;
3. lambda 表达式(匿名函数)
[capture list] (parameter list) -> reurn type
{
function body
}
4. 范围 for 语句
for (declaration : expression){
statement
}
参数的含义:
5. 智能指针
相关知识已在第一章中进行了详细的说明,这里不再重复。
6. nullptr
nullptr 的优势:
7. delete 函数和 default 函数
#include
using namespace std;
class A
{
public:
A() = default; // 表示使用默认的构造函数
~A() = default; // 表示使用默认的析构函数
A(const A &) = delete; // 表示类的对象禁止拷贝构造
A &operator=(const A &) = delete; // 表示类的对象禁止拷贝赋值
};
int main()
{
A ex1;
A ex2 = ex1; // error: use of deleted function 'A::A(const A&)'
A ex3;
ex3 = ex1; // error: use of deleted function 'A& A::operator=(const A&)'
return 0;
}
8. 右值引用
右值引用必须绑定到右值的引用,通过 && 获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。
#include
#include
using namespace std;
int main()
{
int var = 42;
int &l_var = var;
int &&r_var = var; // error: cannot bind rvalue reference of type 'int&&' to lvalue of type 'int' 错误:不能将右值引用绑定到左值上
int &&r_var2 = var + 40; // 正确:将 r_var2 绑定到求和结果上
return 0;
}
9. std::move() 函数
std::move 可以将一个左值强制转化为右值引用,包含在 utility 头文件中。
面向过程是分析出解决问题所需要的步骤,然后用函数把这些步骤实现,使用的时候依次调用即可;面向对象是把问题分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在解决问题中的行为。
面向过程语言的优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;单片机、嵌入式开发、 Linux/Unix等一般采用面向过程开发,性能是最重要的因素。缺点:没有面向对象易维护、易复用、易扩展
面向对象语言的优点:易维护、易复用、易扩展,面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。缺点:性能比面向过程低
面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含成员变量和成员函数。面向对象编程将变量和函数封装到一个类中,并声明它们的访问级别(public、private、protected),对数据成员起到一定的保护作用。而且在类的对象调用成员函数时,只需知道成员函数的名字、参数列表以及返回值类型即可,无需了解其函数的实现原理。
面向对象的三大特性:
在C++中,
::
、*
、.
、?:
这4个运算符不能重载
class A
{
public:
void fun(int tmp);
void fun(float tmp); // 重载 参数类型不同(相对于上一个函数)
void fun(int tmp, float tmp1); // 重载 参数个数不同(相对于上一个函数)
void fun(float tmp, int tmp1); // 重载 参数顺序不同(相对于上一个函数)
int fun(int tmp); // error: 'int A::fun(int)' cannot be overloaded 错误:注意重载不关心函数返回类型
};
#include
using namespace std;
class Base
{
public:
void fun(int tmp, float tmp1) { cout << "Base::fun(int tmp, float tmp1)" << endl; }
};
class Derive : public Base
{
public:
void fun(int tmp) { cout << "Derive::fun(int tmp)" << endl; } // 隐藏基类中的同名函数
};
int main()
{
Derive ex;
ex.fun(1); // Derive::fun(int tmp)
ex.fun(1, 0.01); // error: candidate expects 1 argument, 2 provided,改为 ex.Base::fun(1, 0.01)就可以调用基类中的同名函数。
return 0;
}
#include
using namespace std;
class Base
{
public:
virtual void fun(int tmp) { cout << "Base::fun(int tmp) : " << tmp << endl; }
};
class Derived : public Base
{
public:
void fun(int tmp) override { cout << "Derived::fun(int tmp) : " << tmp << endl; } // 重写基类中的 fun 函数
};
int main()
{
Base *p = new Derived();
p->fun(3); // Derived::fun(int) : 3
return 0;
}
重载、隐藏、重写的区别:
多态:不同继承类的对象对同一消息做出不同的响应,基类的指针指向或绑定到派生类的对象,使得基类指针呈现不同的表现方式。在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据实际的对象类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数。
多态实现过程(虚函数的作用机制):
静态多态与动态多态:
#include
using namespace std;
class Base
{
public:
virtual void fun() { cout << "Base::fun()" << endl; }
virtual void fun1() { cout << "Base::fun1()" << endl; }
virtual void fun2() { cout << "Base::fun2()" << endl; }
};
class Derive : public Base
{
public:
void fun() { cout << "Derive::fun()" << endl; }
virtual void D_fun1() { cout << "Derive::D_fun1()" << endl; }
virtual void D_fun2() { cout << "Derive::D_fun2()" << endl; }
};
int main()
{
Base *p = new Derive();
p->fun(); // Derive::fun() 调用派生类中的虚函数
return 0;
}
虚函数:被virtual关键字修饰的成员函数就是虚函数。
纯虚函数:虚函数在类中声明时加上等于0,就是纯虚函数。含有纯虚函数的类称为抽象类,类中只有接口,没有具体的实现方法,不能实例化对象;继承抽象类的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。此外,对于实现纯虚函数的派生类,该纯虚函数在派生类中被称为虚函数。
为了防止内存泄露,因为对于基类指针指向派生类对象,如果基类析构函数不是虚函数,那么当delete该对象时,只会调用父类的析构函数,不会调用子类的析构函数,会造成内存泄漏。
C++默认的析构函数不是虚函数是因为虚函数需要额外的虚函数表和虚表指针,占用额外的内存。而对于不会被继承的类来说,其析构函数如果是虚函数,就会浪费内存。
override重写父类的虚函数,final表示不希望某个虚函数被重写
默认构造函数(无参构造函数)
初始化构造函数(有参构造函数)
拷贝构造函数
移动构造函数
默认情况下,c++编译器至少给一个类添加3个函数
默认构造函数(无参,函数体为空)
默认析构函数(无参,函数体为空)
默认拷贝构造函数,对属性进行值拷贝(浅拷贝)
构造函数调用规则如下:
class Person {
public:
Person() {
cout << "无参构造函数!" << endl;
mAge = 0;
}
Person(int age) {
cout << "有参构造函数!" << endl;
mAge = age;
}
Person(const Person& p) {
cout << "拷贝构造函数!" << endl;
mAge = p.mAge;
}
//析构函数在释放内存之前调用
~Person() {
cout << "析构函数!" << endl;
}
public:
int mAge;
};
//1. 使用一个已经创建完毕的对象来初始化一个新对象
void test01() {
Person man(100); //p对象已经创建完毕
Person newman(man); //调用拷贝构造函数
Person newman2 = man; //拷贝构造
//Person newman3;
//newman3 = man; //不是调用拷贝构造函数,赋值操作
}
//2. 值传递的方式给函数参数传值
//相当于Person p1 = p;
void doWork(Person p1) {}
void test02() {
Person p; //无参构造函数
doWork(p);
}
//3. 以值方式返回局部对象
Person doWork2()
{
Person p1;
cout << (int *)&p1 << endl;
return p1;
}
void test03()
{
Person p = doWork2();
cout << (int *)&p << endl;
}
int main() {
//test01();
//test02();
test03();
system("pause");
return 0;
}
不能重载,析构函数在对象的生存周期即将结束的时候由系统自动调用,调用结束后对象就消失了,之后的重载的析构函数也就不能被调用了。
访问范围:
继承后的变化:
数组名的意义:
- sizeof(数组名),这里的数组名表示整个数组,计算的是整个数组的大小。
- & 数组名,这里的数组名表示整个数组,取出的是整个数组的地址。
- 除此之外所有的数组名都表示首元素的地址
char* a = "12345";
char b[8] = "12345";
char c[] = "12345";
// 64位编译器
sizeof(a); // 8,一个指针占8个字节
strlen(a); // 5,不包括\0
sizeof(b); // 8,表示在内存中预分配的大小
strlen(b); // 5,不包括\0
sizeof(c); // 6,c是数组且包括\0
strlen(b); // 5,不包括\0
注意:在64位编译环境下,一个指针占8个字节,在32位下占4个字节
sizeof(类)相关
#include
using namespace std;
class A
{
};
class B
{
public:
B() {}
~B() {}
};
class C
{
public:
C() {}
virtual ~C() {}
};
int main()
{
cout <<"sizeof一个空类的大小为 "<< sizeof(A) << endl;//1
cout << "sizeof一个带有构造函数和析构函数的类的大小为 " << sizeof(B) << endl;//1
cout << "sizeof一个带有虚函数的类的大小为 " << sizeof(C) << endl;//4
return 0;
}
sizeof(1==1) 在 C 和 C++ 中分别是什么结果?
C语言为4,C++为1,因为c语言返回的是int,而c++返回的是bool
[capture list] (parameter list) -> reurn type
{
function body
}
使用场景:排序算法
bool compare(int& a, int& b)
{
return a > b;
}
int main(void)
{
int data[6] = { 3, 4, 12, 2, 1, 6 };
vector<int> testdata;
testdata.insert(testdata.begin(), data, data + 6);
// 排序算法
sort(testdata.begin(), testdata.end(), compare); // 降序
// 使用lambda表达式
sort(testdata.begin(), testdata.end(), [](int a, int b){ return a > b; }); // 降序
return 0;
}
用来声明类构造函数是显示调用的,而非隐式调用,可以阻止调用构造函数时进行隐式转换。只可用于修饰单参构造函数,因为无参构造函数和多参构造函数本身就是显示调用的,再加上 explicit 关键字也没有什么意义。
隐式转换:
#include
#include
using namespace std;
class A
{
public:
int var;
A(int tmp)
{
var = tmp;
}
};
int main()
{
A ex = 10; // 发生了隐式转换
return 0;
}
上述代码中,A ex = 10; 在编译时,进行了隐式转换,将 10 转换成 A 类型的对象,然后将该对象赋值给 ex,等同于如下操作:
为了避免隐式转换,可用 explicit 关键字进行声明:
#include
#include
using namespace std;
class A
{
public:
int var;
explicit A(int tmp)
{
var = tmp;
cout << var << endl;
}
};
int main()
{
A ex(100);
A ex1 = 10; // error: conversion from 'int' to non-scalar type 'A' requested
return 0;
}
static 静态成员变量:
#include
using namespace std;
class A
{
public:
static int s_var;
int var;
void fun1(int i = s_var); // 正确,静态成员变量可以作为成员函数的参数
void fun2(int i = var); // error: invalid use of non-static data member 'A::var'
};
#include
using namespace std;
class A
{
public:
static A s_var; // 正确,静态数据成员
A var; // error: field 'var' has incomplete type 'A'
A *p; // 正确,指针
A &var1; // 正确,引用
};
static静态成员函数:
const成员变量:
const成员函数:
#include
#define MAX(X, Y) ((X)>(Y)?(X):(Y))
#define MIN(X, Y) ((X)<(Y)?(X):(Y))
using namespace std;
int main ()
{
int var1 = 10, var2 = 100;
cout << MAX(var1, var2) << endl;
cout << MIN(var1, var2) << endl;
return 0;
}
/*
程序运行结果:
100
10
*/
区别:
const 的优点:
#include
#define INTPTR1 int *
typedef int * INTPTR2;
using namespace std;
int main()
{
INTPTR1 p1, p2; // p1: int *; p2: int
INTPTR2 p3, p4; // p3: int *; p4: int *
int var = 1;
const INTPTR1 p5 = &var; // 相当于 const int * p5; 指针常量,底层const,指针指向地址中存储的值不能改变,指针指向的地址可以改变。
const INTPTR2 p6 = &var; // 相当于 int * const p6; 常量指针,不能改变指针指向的地址,可以改变指针指向地址中存储的值。
return 0;
}
#include
#define MAX(a, b) ((a) > (b) ? (a) : (b))
using namespace std;
inline int fun_max(int a, int b)
{
return a > b ? a : b;
}
int main()
{
int var = 1;
cout << MAX(var, 5) << endl;
cout << fun_max(var, 0) << endl;
return 0;
}
/*
程序运行结果:
5
1
*/
原理:用于定义内联函数,在编译阶段将函数体嵌入到每一个调用该函数的语句块中。
与普通函数的区别:
使用方法:
#include
using namespace std;
class A{
public:
int var;
A(int tmp){
var = tmp;
}
void fun(){
cout << var << endl;
}
};
#include
using namespace std;
class A{
public:
int var;
A(int tmp){
var = tmp;
}
void fun();
};
inline void A::fun(){
cout << var << endl;
}
限制:inline对编译器的一种请求,编译器可能拒绝这种请求:
malloc在申请内存时,一般会通过系统函数brk或者mmap系统调用来申请内存。当申请内存小于128k时,会使用系统函数brk在堆区分配;而当申请内存大于128k时,会使用mmap系统调用在映射区分配。
不会,被free回收的内存会首先被ptmalloc使用双向链表保存起来,当用户下一次申请内存的时候,会尝试从这些内存中寻找合适的返回。这样就避免了频繁的系统调用,占用过多的系统资源。同时ptmalloc也会尝试对小块内存进行合并,避免过多的内存碎片。
说明:union 是联合体,struct 是结构体。
区别:
C++是在C语言的基础上发展起来的,为了与C语言兼容,C++中保留了 struct。
相同点:
不同点:
当class继承struct或者struct继承class时,默认的继承权限取决于派生类的默认继承
struct A{};
class B : A{}; // private 继承
struct C : B{}; // public 继承
加上extern "C"后,会指示编译器这部分代码按C语言而不是C++的方式进行编译。由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
// 可能出现在 C++ 头文件中的链接指示
extern "C"{
int strcmp(const char*, const char*);
}
左值和右值的区别?
如何判断某个值是左值还是右值:
左值引用和右值引用的区别:
#include
using namespace std;
void fun1(int& tmp)
{
cout << "fun1(int& tmp):" << tmp << endl;
}
void fun2(int&& tmp)
{
cout << "fun2(int&& tmp)" << tmp << endl;
}
int main()
{
int var = 11;
fun1(12); // error: cannot bind non-const lvalue reference of type 'int&' to an rvalue of type 'int'
fun1(var);
fun2(1);
}
std::move 可以将一个左值强制转化为右值引用,包含在 utility 头文件中。函数原型如下:
template <typename T>
typename remove_reference<T>::type&& move(T&& t)
{
return static_cast<typename remove_reference<T>::type &&>(t);
}
// remove_reference相关
//原始的,最通用的版本
template <typename T> struct remove_reference{
typedef T type; //定义 T 的类型别名为 type
};
//部分版本特例化,将用于左值引用和右值引用
template <class T> struct remove_reference<T&> //左值引用
{ typedef T type; }
template <class T> struct remove_reference<T&&> //右值引用
{ typedef T type; }
//举例如下,下列定义的a、b、c三个变量都是int类型
int i;
remove_refrence<decltype(42)>::type a; //使用原版本,
remove_refrence<decltype(i)>::type b; //左值引用特例版本
remove_refrence<decltype(std::move(i))>::type b; //右值引用特例版本
引用折叠原理
move原理
this指针:指向类对象的指针常量。空指针是没有this指针的
#include
#include
using namespace std;
class A
{
public:
void set_name(string tmp)
{
this->name = tmp;
}
void set_age(int tmp)
{
this->age = age;
}
void set_sex(int tmp)
{
this->sex = tmp;
}
void show()
{
cout << "Name: " << this->name << endl;
cout << "Age: " << this->age << endl;
cout << "Sex: " << this->sex << endl;
}
private:
string name;
int age;
int sex;
};
int main()
{
A *p = new A();
p->set_name("Alice");
p->set_age(16);
p->set_sex(1);
p->show();
return 0;
}
#include
using namespace std;
class A {
public:
void func() {
cout << "test" << endl;
m_ = 10; // 有这句话会报错,没有的话不会报错。因为没有这句话不涉及this指针,不管类对象是什么类型都会正确执行;有的话涉及this指针,空指针是没有this指针的,所以会报错。
}
public:
int m_;
};
int main() {
A* p = nullptr;
p->func();
}
野指针指的是指针变量未初始化,只需要在定义指针变量时及时初始化。悬空指针指的是指针free或delete后没有及时置空,指针仍然指向之前分配的内存,如果这块内存暂时可以被程序访问并且不会造成冲突,那么之后使用该指针并不会引发错误。为了避免出现“悬空指针”引发不可预知的错误,可以在释放后立即置空
void *p;
// 此时 p 是野指针
void *p = malloc(size);
free(p);
// 此时,p 指向的内存空间已释放, p 就是悬空指针
**指针函数:**本质是一个函数,只不过该函数的返回值是一个指针。
#include
using namespace std;
struct Type
{
int var1;
int var2;
};
Type * fun(int tmp1, int tmp2){
Type * t = new Type();
t->var1 = tmp1;
t->var2 = tmp2;
return t;
}
int main()
{
Type *p = fun(5, 6);
return 0;
}
函数指针:函本质是一个指针,只不过这个指针指向一个函数。函数指针即指向函数的指针。
#include
using namespace std;
int fun1(int tmp1, int tmp2)
{
return tmp1 * tmp2;
}
int fun2(int tmp1, int tmp2)
{
return tmp1 / tmp2;
}
int main()
{
int (*fun)(int x, int y);
fun = fun1;
cout << fun(15, 5) << endl;
fun = fun2;
cout << fun(15, 5) << endl;
return 0;
}
/*
运行结果:
75
3
*/
static_cast:
const_cast:只能用于去除指针或引用的常量性,不能用于去掉变量的常量性。
reinterpret_cast:改变指针或引用的类型,可以将指针或引用转换为一个足够长度的整型或将整型转化为指针或引用类型。
#include
using namespace std;
int main() {
int a = 0x1234;
cout << reinterpret_cast(&a) << endl;
}
输出可能是一些乱码字符或直接崩溃,因为reinterpret_cast
将int
类型的指针转换为char*
类型的指针,然后cout
将其视为一个以null终止的C风格字符串,而0x1234
并不是一个有效的null终止C风格字符串。
dynamic_cast:
#include
#include
using namespace std;
class Base
{
public:
virtual void fun()
{
cout << "Base::fun()" << endl;
}
};
class Derive : public Base
{
public:
virtual void fun()
{
cout << "Derive::fun()" << endl;
}
};
int main()
{
Base *p1 = new Derive();
Base *p2 = new Base();
Derive *p3 = new Derive();
//转换成功
p3 = dynamic_cast(p1);
if (p3 == NULL)
{
cout << "NULL" << endl;
}
else
{
cout << "NOT NULL" << endl; // 输出
}
//转换失败
p3 = dynamic_cast(p2);
if (p3 == NULL)
{
cout << "NULL" << endl; // 输出
}
else
{
cout << "NOT NULL" << endl;
}
return 0;
}
在传递参数时,什么时候使用指针,什么时候使用引用
模板:创建类或者函数的蓝图或者公式,分为函数模板和类模板。实现方式:模板定义以关键字 template 开始,后跟一个模板参数列表。
template
函数模板:通过定义一个函数模板,可以避免为每一种类型定义一个新函数。
#include
using namespace std;
template
T add_fun(const T & tmp1, const T & tmp2){
return tmp1 + tmp2;
}
int main(){
int var1, var2;
cin >> var1 >> var2;
cout << add_fun(var1, var2);
double var3, var4;
cin >> var3 >> var4;
cout << add_fun(var3, var4);
return 0;
}
类模板:类似函数模板,类模板以关键字 template 开始,后跟模板参数列表。但是,编译器不能为类模板推断模板参数类型,需要在使用该类模板时,在模板名后面的尖括号中指明类型。
#include
using namespace std;
template
class Complex
{
public:
//构造函数
Complex(T a, T b)
{
this->a = a;
this->b = b;
}
//运算符重载
Complex operator+(Complex &c)
{
Complex tmp(this->a + c.a, this->b + c.b);
cout << tmp.a << " " << tmp.b << endl;
return tmp;
}
private:
T a;
T b;
};
int main()
{
Complex a(10, 20);
Complex b(20, 30);
Complex c = a + b;
return 0;
}
可变参数模板:接受可变数目参数的模板函数或模板类。将可变数目的参数被称为参数包,包括模板参数包和函数参数包。
用省略号来指出一个模板参数或函数参数表示一个包,在模板参数列表中,class… 或 typename… 指出接下来的参数表示零个或多个类型的列表;一个类型名后面跟一个省略号表示零个或多个给定类型的非类型参数的列表。当需要知道包中有多少元素时,可以使用 sizeof… 运算符
template // Args 是模板参数包
void foo(const T &t, const Args&... rest); // 可变参数模板,rest 是函数参数包
#include
using namespace std;
template
void print_fun(const T &t)
{
cout << t << endl; // 最后一个元素
}
template
void print_fun(const T &t, const Args &...args)
{
cout << t << " ";
print_fun(args...);
}
int main()
{
print_fun("Hello", "wolrd", "!");
return 0;
}
/*运行结果:
Hello wolrd !
*/
说明:可变参数函数通常是递归的,第一个版本的 print_fun 负责终止递归并打印初始调用中的最后一个实参。第二个版本的 print_fun 是可变参数版本,打印绑定到 t 的实参,并用来调用自身来打印函数参数包中的剩余值。
模板特化的原因:模板并非对任何模板实参都合适、都能实例化,某些情况下,通用模板的定义对特定类型不合适,可能会编译失败,或者得不到正确的结果。因此,当不希望使用模板版本时,可以定义类或者函数模板的一个特例化版本。
模板特化:模板参数在某种特定类型下的具体实现。分为函数模板特化和类模板特化
特化分为全特化和偏特化:
说明:要区分下函数重载与函数模板特化定义函数模板的特化版本,本质上是接管了编译器的工作,为原函数模板定义了一个特殊实例,而不是函数重载,函数模板特化并不影响函数匹配。
#include
#include
using namespace std;
//函数模板
template
bool compare(T t1, T t2)
{
cout << "通用版本:";
return t1 == t2;
}
template <> //函数模板特化
bool compare(char *t1, char *t2)
{
cout << "特化版本:";
return strcmp(t1, t2) == 0;
}
int main(int argc, char *argv[])
{
char arr1[] = "hello";
char arr2[] = "abc";
cout << compare(123, 123) << endl;
cout << compare(arr1, arr2) << endl;
return 0;
}
/*
运行结果:
通用版本:1
特化版本:0
*/
泛型编程实现的基础:模板。模板是创建类或者函数的蓝图或者说公式,当时用一个 vector 这样的泛型,或者 find 这样的泛型函数时,编译时会转化为特定的类或者函数。
泛型编程涉及到的知识点较广,例如:容器、迭代器、算法等都是泛型编程的实现实例。面试者可选择自己掌握比较扎实的一方面进行展开。
try,throw和catch关键字:程序先执行try语句块,如果没有throw抛出异常,则不会进入任何catch语句块,如果有的话用catch精准捕获throw异常,如果匹配不到catch参数就报错,可以使用catch(…)捕获任何异常
函数异常声明列表:程序员在定义函数的时候知道函数可能发生的异常,可以在函数声明和定义时,指出所抛出的异常列表
C++标准异常类exception
int fun() throw(int, double, A, B, C) {...}; // A,B,C是类型
只有在运行时才能检测到的错误,包括range_error(生成的结果超出有意义的值域范围)、overflow_error(上溢)、underflow_error(下溢)、system_error(系统错误),没有默认构造函数。
在C++中,函数调用是指通过函数名称和参数来执行函数内部代码的过程。当程序调用一个函数时,CPU首先需要将函数的参数和返回地址等信息保存到栈空间中,并跳转到函数的入口处开始执行函数代码。当函数执行完毕后,程序又会从函数返回的地方继续执行。
易错点:在设定函数的参数默认值后,该参数后面定义的所有参数都必须设定默认值。
同步:当函数发起调用时,在没得到结果之前,该调用永不返回
异步:当函数发起调用时,不用等待结果,就去执行下面的内容
容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据,从实现角度来看是类模板
算法:各种常用算法,如sort、find、copy
迭代器
仿函数
适配器
分配器
定义:哈希表是根据关键码的值而直接进行访问的数据结构。
原理:哈希表把Key通过哈希函数转换成一个整型数字,该数字对数组长度取余,取余结果就当作数组的下标,将value存储在以该数字为下标的数组空间里。当使用哈希表进行查询的时候,利用哈希函数将key转换为对应的数组下标,从而获得value。
哈希冲突:不同的key映射到了同一位置的下标,这一现象叫做哈希碰撞。
如何解决:拉链法(同一位置下标进行链表连接)和线性探索法(如果原下标位置有value,就向下找一个空位放置key)。如何访问呢?
C++底层是哈希表的数据结构:unordered_set和unordered_map
unordered_map和map的区别?底层是什么数据结构?map的key有序体现在哪里?
unordered_map的key是无序的,map的key是有序的;unordered_map的底层是哈希表,map的底层是红黑树;unordered_map的查询和增删效率都是O(1),map都是O(logn)。std::map
中的 key 默认是按照升序排序的。当插入新的键值对时,std::map
会根据 key 进行排序,确保 key 始终按照升序排列。
定义:只包含一个被称为单例的特殊类,它的目的是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。
class Singleton {
public:
static Singleton* getInstance() { // 静态方法,返回唯一实例
return instance;
}
private:
Singleton() {} // 私有的构造函数,防止外部创建实例
static Singleton* instance; // 私有的静态成员变量,保存唯一实例
};
// 下面这个静态成员变量在类加载的时候就已经初始化好了
Singleton* Singleton::instance = new Singleton();
int main() {
// 通过getInstance获取实例
Singleton* singleton = Singleton::getInstance();
return 0;
}
class Singleton {
public:
static Singleton* getInstance() { // 静态方法,返回唯一实例
if (instance == nullptr) instance = new Singleton();
return instance;
}
private:
Singleton() {} // 私有的构造函数,防止外部创建实例
static Singleton* instance; // 私有的静态成员变量,保存唯一实例
};
Singleton* Singleton::instance = nullptr;
int main() {
// 通过getInstance获取实例
Singleton* singleton = Singleton::getInstance();
return 0;
}
饿汉模式和懒汉模式的区别:
在饿汉模式中,单例对象在程序启动时就会被立即创建和初始化,不管是否会被用到。优点是线程安全,因为实例已经在使用之前就被创建,不会存在多线程同时创建的问题。缺点是可能会造成不必要的资源浪费,尤其是在单例对象的初始化过程较为耗时或占用较多资源的情况下。
在懒汉模式中,单例对象的创建被延迟到了真正被需要的时候才进行。优点是可以避免不必要的资源浪费,只有当需要使用单例对象时才会进行实例化。缺点是懒汉模式需要考虑多线程并发访问问题。