答案:
内存泄露
一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定)内存块,使用完后必须显式释放的内存。应用程序般使用malloc,、realloc、 new等函数从堆中分配到块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
避免内存泄露的几种方式
答案:
对象复用
对象复用其本质是一种设计模式:Flyweight享元模式。
通过将对象存储到“对象池”中实现对象的重复利用,这样可以避免多次创建重复对象的开销,节约系统资源。
零拷贝
零拷贝就是一种避免 CPU 将数据从一块存储拷贝到另外一块存储的技术。
零拷贝技术可以减少数据拷贝和共享总线操作的次数。
在C++中,vector的一个成员函数 emplace_back() 很好地体现了零拷贝技术,它跟push_back()函数一样可以将一个元素插入容器尾部,区别在于:使用push_back()函数需要调用拷贝构造函数和转移构造函数,而使用emplace_back()插入的元素原地构造,不需要触发拷贝构造和转移构造,效率更高。举个例子:
#include
#include
#include
using namespace std;
struct Person
{
string name;
int age;
//初始构造函数
Person(string p_name, int p_age): name(std::move(p_name)), age(p_age)
{
cout << "I have been constructed" <<endl;
}
//拷贝构造函数
Person(const Person& other): name(std::move(other.name)), age(other.age)
{
cout << "I have been copy constructed" <<endl;
}
//转移构造函数
Person(Person&& other): name(std::move(other.name)), age(other.age)
{
cout << "I have been moved"<<endl;
}
};
int main()
{
vector<Person> e;
cout << "emplace_back:" <<endl;
e.emplace_back("Jane", 23); //不用构造类对象
vector<Person> p;
cout << "push_back:"<<endl;
p.push_back(Person("Mike",36));
return 0;
}
//输出结果:
//emplace_back:
//I have been constructed
//push_back:
//I have been constructed
//I am being moved.
答案:
reinterpret_cast:
用于将一个对象的内存表示重新解释为另一个类型的对象。它可以在不改变对象值的情况下,将一个类型转换为另一个类型。例如:
int a = 1;
double b = reinterpret_cast<double&>(a); // 将整数a的内存表示转换为双精度浮点数b
const_cast:
该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外.
用于移除对象的const属性。它可以在需要修改const对象时使用,例如:
const int a = 1;
int* p = const_cast<int*>(&a); // 将const整数a的地址转换为非const整数指针p
*p = 2; // 修改p指向的值
static_cast:
没有运行时类型检查来保证转换的安全性
用于在编译时进行类型转换。它可以在需要显式类型转换时使用,例如:
int a = 1;
double b = static_cast<double>(a); // 将整数a转换为双精度浮点数b
dynamic_cast:
有类型检查,基类向派生类转换比较安全,但是派生类向基类转换则不太安全
用于在运行时进行类型转换。它可以在需要根据对象的实际类型进行类型转换时使用,例如:
class Base { virtual void print() {} };
class Derived : public Base {};
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 将基类指针basePtr转换为派生类指针derivedPtr
答案:
函数的调用过程:
1)从栈空间分配存储空间
2)从实参的存储空间复制值到形参栈空间
3)进行运算
形参在函数未调用之前都是没有分配存储空间的,在函数调用结束之后,形参弹出栈空间,清除形参空间。
数组作为参数的函数调用方式是地址传递,形参和实参都指向相同的内存空间,调用完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。
当函数有多个返回值的时候,不能用普通的 return 的方式实现,需要通过传回地址的形式进行,即地址/指针传递。
#include
using namespace std;
int f(int n)
{
cout << n << endl;
return n;
}
void func(int param1, int param2)
{
int var1 = param1;
int var2 = param2;
printf("var1=%d,var2=%d", f(var1), f(var2));//如果将printf换为cout进行输出,输出结果则刚好相反
}
int main(int argc, char* argv[])
{
func(1, 2);
return 0;
}
//输出结果
//2
//1
//var1=1,var2=2
答案:
class Person {
public:
Person() : name(""), age(0) {}
Person(const std::string& n, int a) : name(n), age(a) {}
Person(Person&& other) noexcept : name(std::move(other.name)), age(other.age) {
other.name = "";
other.age = 0;
}
private:
std::string name;
int age;
};
答案:
使用
示例如下:
#include
#include
using namespace std;
struct S
{
int x;
char y;
int z;
double a;
};
int main()
{
cout << offsetof(S, x) << endl; // 0
cout << offsetof(S, y) << endl; // 4
cout << offsetof(S, z) << endl; // 8
cout << offsetof(S, a) << endl; // 12
return 0;
}
答案:
从上面的定义也可以看出,非虚函数一般都是静态绑定,而虚函数都是动态绑定(如此才可实现多态性)。 举个例子:
#include
using namespace std;
class A
{
public:
/*virtual*/ void func() { std::cout << "A::func()\n"; }
};
class B : public A
{
public:
void func() { std::cout << "B::func()\n"; }
};
class C : public A
{
public:
void func() { std::cout << "C::func()\n"; }
};
int main()
{
C* pc = new C(); //pc的静态类型是它声明的类型C*,动态类型也是C*;
B* pb = new B(); //pb的静态类型和动态类型也都是B*;
A* pa = pc; //pa的静态类型是它声明的类型A*,动态类型是pa所指向的对象pc的类型C*;
pa = pb; //pa的动态类型可以更改,现在它的动态类型是B*,但其静态类型仍是声明时候的A*;
C *pnull = NULL; //pnull的静态类型是它声明的类型C*,没有动态类型,因为它指向了NULL;
pa->func(); //A::func() pa的静态类型永远都是A*,不管其指向的是哪个子类,都是直接调用A::func();
pc->func(); //C::func() pc的动、静态类型都是C*,因此调用C::func();
pnull->func(); //C::func() 不用奇怪为什么空指针也可以调用函数,因为这在编译期就确定了,和指针空不空没关系;
return 0;
}
如果将A类中的virtual注释去掉,则运行结果是:
a->func(); //B::func() 因为有了virtual虚函数特性,pa的动态类型指向B*,因此先在B中查找,找到后直接调用;
pc->func(); //C::func() pc的动、静态类型都是C*,因此也是先在C中查找;
pnull->func(); //空指针异常,因为是func是virtual函数,因此对func的调用只能等到运行期才能确定,然后才发现pnull是空指针;
在上面的例子中,
至此总结一下静态绑定和动态绑定的区别:
答案:
include <iostream>
using namespace std;
int main()
{
int *a, *b, c;
a = (int*)0x500;
b = (int*)0x520;
c = b - a;
printf("%d\n", c); // 8
a += 0x020;
c = b - a;
printf("%d\n", c); // -24
return 0;
}
首先变量a和b都是以16进制的形式初始化,将它们转成10进制分别是1280(5162=1280)和1312(5*162+216=1312), 那么它们的差值为32,也就是说a和b所指向的地址之间间隔32个位,但是考虑到是int类型占4位,所以c的值为32/4=8
a自增16进制0x20之后,其实际地址变为1280 + 2164 = 1408,(因为一个int占4位,所以要乘4),这样它们的差值就变成了1312 - 1408 = -96,所以c的值就变成了-96/4 = -24
答案:
在计算机编程中,由于浮点数的精度问题,直接比较两个浮点数是否相等可能会得到错误的结果。因此,通常采用一种称为“浮点数比较”的方法来判断两个浮点数是否相等。
以下是一种常见的方法:
举个栗子:
#include
#include
bool float_equal(float a, float b, float epsilon = 1e-6) {
return std::abs(a - b) < epsilon;
}
int main() {
float num1 = 0.1 + 0.2;
float num2 = 0.3;
if (float_equal(num1, num2)) {
std::cout << "两个浮点数相等" << std::endl;
} else {
std::cout << "两个浮点数不相等" << std::endl;
}
return 0;
}
答案:
对象之间的转换主要有两种类型:向上类型转换和向下类型转换。
向上类型转换:
将派生类指针或引用转换为基类的指针或引用被称为向上类型转换。这种转换会自动进行,是安全的。例如,如果我们有一个指向派生类对象的基类指针,我们可以直接使用这个指针来操作派生类对象,无需进行任何额外的转换。
向下类型转换:
将基类指针或引用转换为派生类指针或引用被称为向下类型转换。这种转换需要显式进行,且只有在运行时才能确定是否成功。向上类型转换和向下类型转换的主要区别在于,向上类型转换是自动的、安全的,而向下类型转换需要显式进行,并可能存在风险。
指针和引用之间的转换主要有两种形式:指针转引用和引用转指针。
指针转引用:
这是将一个指针赋值给一个引用。例如,当我们有一个指向某个对象的指针p时,我们可以创建一个引用r,使得r指向与p相同的对象。具体来说,我们可以通过使用*操作符来完成这个转换,如:int a = 10; int *p = &a; int &r = *p;。
引用转指针:
这是将一个引用赋值给一个指针。例如,如果我们有一个引用r,我们可以创建一个指针p,使得p指向与r相同的对象。为了实现这一点,我们可以使用&操作符来获取r的地址,如:int a = 10; int &r = a; int *p = &r;。