缺省参数
全缺省、半缺省
- 缺省参数不能在函数声明和定义中同时存在(编译器无法确定使用哪个缺省值)。
- 半缺省参数必须从右往左依次来给,不能间隔。
函数重载
同一作用域的功能类似同名函数,同名函数的参数不同(参数个数、类型、类型顺序)。
为什么C语言不支持函数重载而C++支持函数重载?
C/C++程序运行起来,要经过:预处理(.i)、编译(.s)、汇编(.o)、链接。
- C 语言由于简单的函数名处理机制不支持函数重载,编译器对函数名的处理简单,只使用函数原名进行标识,如果有同名函数,编译器链接的时候就会出现多个同名符号,导致链接错误。
- 而 C++ 是通过函数名修饰机制(编译器会根据函数名、参数类型和数量等信息对函数名进行修饰,生成一个唯一的标识符,这样在链接时就可以根据唯一标识符来区分不同参数的同名函数。)来区分只要参数不同修饰出来的函数名字就不同,从而是实现了函数重载。
- 如果两个函数函数名和参数是一样的,返回值不同是不构成重载的,因为调用时编译器没办法区分。
引用
给变量取别名,与引用的变量共用一块内存空间,引用类型和引用变量类型相同。
- 定义时初始化
- 一个变量可以有多个引用
- 一个引用只能引用一个实体
- 常引用
cpp
const int c = 1;
//int& rc = c;//error
const int& rc = c;
cpp
int& Add(int a, int b) {
int c = a + b;
return c; }
int main()
{
int& ret = Add(1, 2);
Add(3, 4);
cout << "Add(1, 2) is :"<< ret <<endl;//返回一个已经销毁的局部变量的引用会导致未定义行为,因为引用指向的内存已经无效。
return 0; }
如果函数返回时,出了函数作用域,如果返回对象还在(还没还给系统),则可以使用 引用返回,如果已经还给系统了,则必须使用传值返回。
传值和传引用的效率比较
- 以值作为参数或者返回值类型,在传参和返回期间,函数不会直接传递实参或者将变量本身直 接返回,而是传递形参(形参是实参的一份临时拷贝,涉及到内存的读写操作)或者返回变量的一份临时的拷贝,因此用值作为参数或者返回值类型,效率是非常低下的,尤其是当参数或者返回值类型非常大时,效率就更低。
- 传引用,传的是实参的引用,引用和实参共用一份内存空间,也就是实参的内存地址,不涉及实参的复制操作。避免了复制带来的时间开销。
引用和指针的不同
- 引用概念上定义一个变量的别名,指针存储一个变量地址。
- 引用在定义时必须初始化,指针没有要求
- 引用在初始化时引用一个实体后,就不能再引用其他实体,而指针可以在任何时候指向任何一个同类型实体
- 没有NULL引用,但有NULL指针
- 在sizeof中含义不同:引用结果为引用类型的大小,但指针始终是地址空间所占字节个数(32
- 位平台下占4个字节)
- 引用自加即引用的实体增加1,指针自加即指针向后偏移一个类型的大小
- 有多级指针,但是没有多级引用
- 访问实体方式不同,指针需要显式解引用,引用编译器自己处理
- 引用比指针使用起来相对更安全
内联函数
以inline修饰,一种特殊的函数,在编译时,编译器会尝试将内联函数的函数体代码直接插入到调用该函数的地方,而不是像普通函数那样进行函数调用的跳转操作。这样可以减少函数调用的开销(保存恢复寄存器、参数传递),提高程序的执行效率。
- 如果内联函数的函数体比较大,或者在多个地方频繁调用内联函数,会导致代码量大幅增加,占用更多的内存空间。
- inline是对编译器的建议,不是强制要求,编译器会根据实际情况决定是否将内联函数代码展开。
- 内联函数不建议声明和定义分离,编译器编译的时候会导致链接错误。inline被展开没有函数地址了,链接找不到。
面试题:宏的优缺点?
优点:
1.增强代码的复用性。
2.提高性能。
缺点:
1.不方便调试宏。(因为预编译阶段进行了替换)
2.导致代码可读性差,可维护性差,容易误用。
3.没有类型安全的检查 。
C++有哪些技术替代宏?
- 常量定义 换用const enum
- 短小函数定义 换用内联函数
auto(C++11)
- 类型难于拼写
- 含义不明确导致容易出错
使用auto定义变量时必须对其进行初始化,在编译阶段编译器需要根据初始化表达式来推导auto 的实际类型。因此auto并非是一种"类型"的声明,而是一个类型声明时的"占位符",编译器在编译期会将auto替换为变量实际的类型。
- 不能作为函数参数
- 不能直接声明数组
C++实现封装的方式
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来 隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。class的默认访问权限为private,struct为public(因为struct要兼容C)访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。
面试题:C++struct和class的区别
解答:C++需要兼容C语言,所以C++中struct可以当成结构体使用。另外C++中struct还可以用来定义类。和class定义类是一样的,区别是struct定义的类默认访问权限是public,class定义的类默认访问权限是private。
面试题:面向对象的三大特性
封装、继承、多态。
封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理,让用户更方便使用类。
类对象的大小
一个类的大小,实际就是该类中"成员变量"之和,当然要注意内存对齐。
注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。
面试题:结构体内存对齐
结构体内存对齐规则
- 第一个成员:第一个成员在与结构体变量偏移量为 0 的地址处。
- 其他成员:其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。在 VS 中,默认对齐数为 8。
- 结构体总大小:结构体总大小为最大对齐数(每个成员变量都有一个对齐数)的整数倍。
- 嵌套结构体:如果嵌套了结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
- 平台原因**(移植原因)**:
不是所有的硬件平台都能访问任意地址上的任意数据的,某些硬件平台只能在某些地址处取某些特
- 性能原因:
数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
面试题:如何让结构体按照指定的对齐参数进行对齐?
能否按照3、4、5即任意字节对齐?
能。pragma pack(4);//设置默认对齐数 pragma pack();//恢复默认对齐数。虽然理论上有些编译器可能支持指定 3 或 5 字节对齐,但并不是所有编译器都能保证支持。因为在实际的硬件架构中,对齐通常是按照 2 的幂次方进行优化的,这样可以提高内存访问的效率。在 GCC 编译器中,
#pragma pack
主要支持 2 的幂次方值(如 1、2、4、8、16 等),对于非 2 的幂次方值(如 3、5),可能无法正常工作或者产生未定义行为。
联合
联合也是一种特殊的自定义类型这种类型定义的变量也包含一系列的成员,特征是这些成员公用同一块空间(所以联合也叫共用体)。
联合的大小至少是最大成员的大小。
当最大成员大小不是最大对齐数的整数倍的时候,就要对齐到最大对齐数的整数倍。
面试题:什么是大小端?如何测试某台机器是大端还是小端,有没有遇到过要考虑大小端的场景。
在计算机系统中,数据通常以字节为单位存储。对于多字节的数据类型(如 int
、long
等),它们由多个字节组成,这些字节在内存中的存储顺序有两种方式,即大端字节序(Big - Endian)和小端字节序(Little - Endian)。
也称为大端序或大端模式,在大端字节序中,数据的高位字节存放在内存的低地址处,低位字节存放在内存的高地址处。这就如同我们书写数字一样,高位在前,低位在后。例如,对于一个 4 字节的整数 0x12345678
,在大端字节序的内存中存储如下。
也称为小端序或小端模式,在小端字节序中,数据的低位字节存放在内存的低地址处,高位字节存放在内存的高地址处。对于上述 4 字节的整数 0x12345678
,在小端字节序的内存中存储如下:

使用指针
cpp
// 测试机器大小端的函数
int check_endian_pointer() {
int num = 1;
char *ptr = (char *)#
// 如果低地址存储的是 1,说明是小端序
return (*ptr == 1);
}
使用联合体
cpp
// 测试机器大小端的函数
int check_endian() {
union {
int i;
char c;
} un;
un.i = 1;
// 如果低地址存储的是 1,说明是小端序
return (un.c == 1);
}
this指针
- this指针的类型:类型* const,即成员函数中,不能给this指针赋值。
- 只能在"成员函数"的内部使用。非静态成员函数没有this指针。
- this指针本质上是"成员函数"的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
- this指针是"成员函数"第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递。
面试题:this指针存在哪里?
(也就是存在进程地址空间的哪个区域?)
答:栈上的,因为他是一个形参。(ps:vs下是在ecx这个寄存器来传递)
this指针可以为空吗?
cpp
class A
{
public:
void PrintA()
{
cout<<_a<<endl;
//cout<<"PrintA()"<<endl;
}
private:
int _a;
};
int main()
{
A* p = nullptr;
p->PrintA();
return 0; }
当成员函数不访问成员变量时,this指针可以为空;当成员函数访问成员变量时,this指针不能为空,访问成员变量是,this->_a会解引用空指针,导致未定义行为,引发程序崩溃。
默认成员函数
用户没有显示实现,编译器会生成的成员函数。
构造函数
编译器默认生成的默认的构造函数(无参构造函数),对内置类型没什么用,会对自定义类型成员,调用它的默认成员函数。
- 函数名与类名相同。
- 无返回值。
- 对象实例化时编译器自动调用对应的构造函数。
- 构造函数可以重载。
无参、全缺省、不写编译器默认生成的(无参)都是默认构造函数,默认构造函数只能有一个。
析构函数
- 析构函数名是在类名前加上字符 ~。
- 无参数无返回值类型。
- 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载
- 对象生命周期结束时,C++编译系统系统自动调用析构函数,编译器生成的默认析构函数,对自定类型成员调用它的析构函数。
拷贝构造函数
只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
- 拷贝构造函数是构造函数的一个重载形式。
- 拷贝构造函数的参数只有一个 且必须是类类型对象的引用 ,使用传值方式编译器直接报错, 因为会引发无穷递归调用。
若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按 字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
注意:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请
时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
运算符重载
C++为了增强代码的可读性引入了运算符重载 ,运算符重载是具有特殊函数名的函数
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数。
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义。
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this。
- .* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
cpp
bool operator==(const Date& d2)
{
return _year == d2._year;
&& _month == d2._month
&& _day == d2._day;
}
赋值运算符重载
- 参数类型:const T&,传递引用可以提高传参效率
- 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
- 检测是否自己给自己赋值
- 返回*this :要复合连续赋值的含义
cpp
Date& operator=(const Date& d)
{
if(this != &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
赋值运算符只能重载成类的成员函数不能重载成全局函数
cpp
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right) {
if (&left != &right)
{
left._year = right._year;
left._month = right._month;
left._day = right._day;
}
return left; }
// 编译失败:
// error C2801: "operator ="必须是非静态成员
原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。
用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。注
意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符
重载完成赋值。
既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实
现吗?
注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。
前置++和后置++重载
cpp
// 前置++:返回+1之后的结果
// 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
Date& operator++()
{
_day += 1;
return *this;
}
// 后置++:
// 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
// C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器
自动传递
// 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存
一份,然后给this+1
// 而temp是临时对象,因此只能以值的方式返回,不能返回引用
Date operator++(int)
{
Date temp(*this);
_day += 1;
return temp;
}
取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成初始化列表。
类中包含以下成员,必须放在初始化列表位置进行初始化:
- 引用成员变量
- const成员变量
自定义类型成员(且该类没有默认构造函数时成员变量在类中声明次序就是其在初始化列表中的初始化顺序,与其在初始化列表中的先后次序无关。
explicit****关键字
构造函数不仅可以构造与初始化对象,对于单个参数或者除第一个参数无默认值其余均有默认值 的构造函数,还具有类型转换的作用。
// 隐式类型转换:将 int 类型的值 10 转换为 MyClass 类型的对象
MyClass obj2 = 10;
obj2.printData();
用explicit修饰构造函数,将会禁止构造函数的隐式转换
static
- 静态成员变量一定要在类外进行初始化。
- 静态成员为所有类对象所共享,不属于某个具体的对象,存放在静态区。
- 静态成员变量必须在类外定义,定义时不添加static关键字,类中只是声明
- 类静态成员即可用 类名::静态成员 或者 对象.静态成员 来访问
- 静态成员函数没有隐藏的this指针,不能访问任何非静态成员
- 静态成员也是类的成员,受public、protected、private 访问限定符的限制
- 静态成员函数可以通过类名调用,也可以通过对象调用。但在调用时并不会关联到某个特定的对象。
面试题:实现一个类,计算程序中创建出了多少个类对象
cpp
class A
{
public:
A()
{
++_scount;
}
A(const A& t)
{
++_scount;
}
~A()
{
--_scount;
}
int getadd()
{
return _scount;
}
static int GetACount()
// 静态成员函数可以直接访问静态成员变量,而不需要通过对象来访问。
// 静态成员函数不与任何对象绑定,它没有隐含的 this 指针。
// 这意味着调用静态成员函数时不需要创建类的对象,可以直接通过类名来调用
{
return _scount;
}
private:
static int _scount;
};
int A::_scount = 0;
void TestA()
{
cout << A::GetACount() << endl;
A a1, a2;
A a3(a1);
cout << A::GetACount() << endl;
}
int main()
{
TestA();
return 0;
}
面试题:静态成员函数可以调用非静态成员函数吗?
- 静态成员函数不可以直接调用非静态成员函数。
- 静态成员函数属于整个类,不依赖于类的任何对象,它没有隐含的
this
指针。
如果静态成员函数确实需要调用非静态成员函数,需要通过传递对象指针或引用的方式来实现。示例如下:
cpp
#include <iostream>
class MyClass {
public:
static void staticFunc(MyClass& obj) {
obj.nonStaticFunc();
}
void nonStaticFunc() {
std::cout << "Non-static function called." << std::endl;
}
};
int main() {
MyClass obj;
MyClass::staticFunc(obj);
return 0;
}
非静态成员函数可以调用类的静态成员函数吗?
非静态成员函数可以直接使用类名或者省略类名来调用静态成员函数。静态成员函数属于整个类,而不是某个具体的对象实例。它不依赖于任何对象,没有隐含的 this
指针。
面试题:sizeof与strlen
- 计算内容 :
sizeof
计算的是数据类型或变量在内存中占用的字节数,对于数组,它返回整个数组占用的字节数;对于指针,它返回指针本身的大小。strlen
计算的是以'\0'
结尾的字符串的实际长度,不包括'\0'
。- 适用范围 :
sizeof
可以用于任何数据类型,包括基本数据类型、自定义数据类型、数组、指针等。strlen
只能用于以'\0'
结尾的字符串。- 执行时机 :
sizeof
是编译时运算符,其结果在编译时就确定了,不会对表达式进行实际计算。
strlen
是运行时函数,需要在程序运行时遍历字符串直到遇到'\0'
才能确定长度。
malloc/free和new/delete的区别
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
- malloc和free是函数,new和delete是操作符
- malloc申请的空间不会初始化,new可以初始化
- malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
- malloc的返回值为void*, 在使用时必须强转,new不需要,因为new后跟的是空间的类型
- malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
- 申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理。