1.什么情况下会调用拷贝构造函数
- 用类的一个对象去初始化另一个对象的时候
- 当函数的参数是类的对象时,就是值传递的时候,如果是引用传递则不会调用
- 当函数的返回值是类的对象或者引用?的时候(在c++编译器发生NRV优化(Named return Value优化),如果是引用返回的形式则不会调用拷贝构造函数,如果是值传递的方式依然会发生拷贝构造函数。)
class A
{
public:
A() {};
A(const A& a)
{
cout << "copy constructor is called" << endl;
};
~A() {};
};
void useClassA(A a) {}
A getClassA()//此时会发生拷贝构造函数的调用,虽然发生NRV优化,但是依然调用拷贝构造函数
{
A a;
return a;
}
//A& getClassA2()// VS2019下,此时编辑器会进行(Named return Value优化)NRV优化,不调用拷贝构造函数 ,如果是引用传递的方式返回当前函数体内生成的对象时,并不发生拷贝构造函数的调用
//{
// A a;
// return a;
//}
int main()
{
A a1,a3,a4;
A a2 = a1; //调用拷贝构造函数,对应情况1
useClassA(a1);//调用拷贝构造函数,对应情况2
a3 = getClassA();//发生NRV优化,但是值返回,依然会有拷贝构造函数的调用 情况3
a4 = getClassA2(a1);//发生NRV优化,且引用返回自身,不会调用
return 0;
}
2.volatile、mutable和explicit关键字的用法
- volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新从它所在的内存读取数据,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。
- 如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成const的。但是,有些时候,我们需要在const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后后面关键字位置
- C++中,一个参数的构造函数(或者除了第一个参数外其余参数都有缺省值的多参构造函数),承担了两个角色:1. 用于构建单参数的类对象 2.隐含的类型转换操作符
- 被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显示的方式进行类型转换。explicit只能写在在声明中,不能写在定义中。
- explicit关键字只对有一个参数的类构造函数有效, 如果类构造函数参数大于或等于两个时, 是不会产生隐式转换的, 所以explicit关键字也就无效了
- 声明为explicit的构造函数不能在隐式转换中使用,只能显示调用,去构造一个类对象
Base base(‘a’) //显示调用,OK Base base = ‘a’ //隐是调用,err
3.内联函数和宏定义的区别
- 而内联函数可以进行参数类型检查(编译时),且具有返回值。宏只做简单字符串替换(编译前)。
- 内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载。
- 宏定义时要注意书写(参数要括起来)否则容易出现歧义,内联函数不会产生歧义
4.浅拷贝和深拷贝的区别
- 浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针的资源就会出现错误。
- 深拷贝不仅拷贝值,还开辟出一块新的空间用来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。
#include <iostream>
#include <string.h>
using namespace std;
class Student
{
private:
int num;
char *name;
public:
Student(){
name = new char(20);
cout << "Student" << endl;
};
~Student(){
cout << "~Student " << &name << endl;
delete name;
name = NULL;
};
Student(const Student &s){//拷贝构造函数
//浅拷贝,对象的name和传入对象的name指向相同的地址
name = s.name;
//深拷贝
//name = new char(20);
//memcpy(name, s.name, strlen(s.name));
cout << "copy Student" << endl;
};
};
int main()
{
{// 花括号让s1和s2变成局部对象,方便测试
Student s1;
Student s2(s1);// 复制对象
}
system("pause");
return 0;
}
//浅拷贝执行结果: 地址相同 为什么报错????
//Student
//copy Student
//~Student 0x7fffed0c3ec0
//~Student 0x7fffed0c3ed0
//*** Error in `/tmp/815453382/a.out': double free or corruption (fasttop): 0x0000000001c82c20 ***
//深拷贝执行结果:地址不同
//Student
//copy Student
//~Student 0x7fffebca9fb0
//~Student 0x7fffebca9fc0
5.C++中的重载、重写(覆盖)和隐藏的区别
- 重载是指在 1.同一范围 2.函数名相同 3.参数类型和数目有所不同 4. 不能仅仅依靠返回值不同来区分的函数。重载和函数成员是否是虚函数无关。全局函数和成员函数即使名称和参数相同,两者也是互不干扰(范围不同)。
//如果同名函数仅仅是返回值类型不同,有时可以区分,有时却不能。例如:
void Function(void);
int Function (void);
//上述两个函数,第一个没有返回值,第二个的返回值是int 类型。如果这样调用函数:
int x = Function ();
//则可以判断出Function 是第二个函数。问题是在C++/C 程序中,我们可以忽略函数的返回值。在这种情况下,编译器和程序员都不知道哪个Function 函数被调用。
- 覆盖 指的是在派生类中覆盖基类中的同名函数,覆盖就是覆盖函数体,要求基类函数必须是虚函数且:1.不同范围(指派生类和基类) 2.函数名,参数均相同 3.基类函数的virtual关键词必须有
- 重载 覆盖的区别:重载根据调用时实参表与形参表的对应关系来选择函数体,覆盖关系中,调用方法根据对象类型决定,
- 隐藏 是指派生类的函数隐藏了基类的同名函数。特征:1.不同范围(派生类和基类)2.函数名相同 3. 若参数不同,则基类函数被隐藏;若参数相同,且基类没有
virtual
关键词,则隐藏。(有virtual
被覆盖) - 基类指针只能调用基类的被隐藏函数,无法识别派生类中的隐藏函数。两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。
// 父类
class A {
public:
virtual void fun(int a) { // 虚函数
cout << "This is A fun " << a << endl;
}
void add(int a, int b) {
cout << "This is A add " << a + b << endl;
}
};
// 子类
class B: public A {
public:
void fun(int a) override { // 覆盖
cout << "this is B fun " << a << endl;
}
void add(int a) { // 隐藏
cout << "This is B add " << a + a << endl;
}
};
int main() {
// 基类指针指向派生类对象时,基类指针可以直接调用到派生类的覆盖函数,
// 也可以通过 :: 调用到基类被覆盖的虚函数;
// 而基类指针只能调用基类的被隐藏函数,无法识别派生类中的隐藏函数。
A *p = new B();
p->fun(1); // 调用子类 fun 覆盖函数
p->A::fun(1); // 调用父类 fun
p->add(1, 2);
// p->add(1); // 错误,识别的是 A 类中的 add 函数,参数不匹配
// p->B::add(1); // 错误,无法识别子类 add 函数
return 0;
}
6.C++中新增了string,它与C语言中的 char *有什么区别吗?
- string可以进行动态扩展,是原空间大小两倍的空间(2^n)
- 将string对象声明为变量,而非数组,是一个表示字符串的实体,而char数组是一组用于存储字符串的char存储单元。
- C语言中的字符串是以‘\0’结束的字符数组。
- 要将C++ string转化成 c string的方法:
str.c_str();
- trlen()求字符串的长度,长度不包含\0。 sizeof()求字符串的长度,长度包含\0
- 当指针指向常量字符串时不能修改字符串的值; 但却可以把另外一个字符串付给它
char * pstr = "54321abCdEf";
pstr = "aa";
pstr[1] = "a"; //报错
char str[] = "54321abCdEf";
7. "a" 与 'a' 的区别。
- ‘a’ 是单个字符,可以赋值给一个字符变量。 "a"是一个字符串,字符串以‘\0’结尾。
- ‘a’的长度为1,"a"的长度为2。
- 字符串可以是"abcde"这样的,'abcde'这样就是错误的。
- 在C++中sizeof('a')=1,它是占一个字节,sizeof("a")=2,它后面还有一个\0结束符;而在C语言中,sizeof('a')=4(字符在C语言中是用int型数据存储的)。
8.malloc、realloc、calloc的区别
共同点:malloc/calloc/realloc都是进行动态内存管理的,均在堆上开辟空间,且必须使用free将申请的空间释放。<注意:若是栈上开辟的空间,栈上空间具有作用域,在函数结束时会自动释放掉,由编译器自动维护>
函数原型
void* malloc(size_t size);
malloc在内存的动态存储区中分配一块长度为size字节的连续区域,返回该区域的地址;
实例:int* ptr1 = (int*)malloc(sizeof (int))
void* calloc(size_t nmemb, size_t size);
calloc与malloc类似,参数size为申请地址的单元元素长度,nmemb是参数个数;
实例:int* ptr2 = (int*)calloc(4, sizeof(int));
void* realloc(void* p, size_t newsize);
realloc是给一个已经分配了的地址的指针重新分配空间,参数p为原有空间的地址,newsize是重新申请的地址空间;
实例:int* ptr3 = (int*)realloc(ptr2, sizeof(int)*4)
区别:
- malloc不能初始化所分配的内存空间,需要用memset初始化。如果这块空间被初始化过,则可能遗留各种各样的数据。
- calloc会将所分配的空间中的每一位都初始化为0。
- realloc可以对给定的指针所指向的空间进行扩大或缩小,原有的内存中内容将保持不变。realloc并不保存新的内存空间和原来的内存空间保持同一内存地址,返回的指针可能指向新的地址。
9. C++中有几种类型的new
new有三种典型的使用方法:plain new,nothrow new和placement new
1)plain new
言下之意就是普通的new,就是我们常用的new,在C++中定义如下:
void* operator new(std::size_t) throw(std::bad_alloc);
void operator delete(void *) throw();
因此plain new在空间分配失败的情况下,抛出异常std::bad_alloc而不是返回NULL,因此通过判断返回值是否为NULL是徒劳的,举个例子:
#include <iostream>
#include <string>
using namespace std;
int main()
{
try
{
char *p = new char[10e11];
delete p;
}
catch (const std::bad_alloc &ex)
{
cout << ex.what() << endl;
}
return 0;
}
//执行结果:bad allocation
(2)nothrow new
nothrow new在空间分配失败的情况下是不抛出异常,而是返回NULL,定义如下:
void * operator new(std::size_t,const std::nothrow_t&) throw();
void operator delete(void*) throw();
举个例子:
#include <iostream>
#include <string>
using namespace std;
int main()
{
char *p = new(nothrow) char[10e11];
if (p == NULL)
{
cout << "alloc failed" << endl;
}
delete p;
return 0;
}
//运行结果:alloc failed
(3)placement new
这种new允许在一块已经分配成功的内存上重新构造对象或对象数组。placement new不用担心内存分配失败,因为它根本不分配内存,它做的唯一一件事情就是调用对象的构造函数。定义如下:
void* operator new(size_t,void*);
void operator delete(void*,void*);
使用placement new需要注意两点:
- palcement new的主要用途就是反复使用一块较大的动态分配的内存来构造不同类型的对象或者他们的数组
- placement new构造起来的对象数组,要显式的调用他们的析构函数来销毁(析构函数并不释放对象的内存),千万不要使用delete,这是因为placement new构造起来的对象或数组大小并不一定等于原来分配的内存大小,使用delete会造成内存泄漏或者之后释放内存时出现运行时错误。
举个例子:
#include <iostream>
#include <string>
using namespace std;
class ADT{
int i;
int j;
public:
ADT(){
i = 10;
j = 100;
cout << "ADT construct i=" << i << "j="<<j <<endl;
}
~ADT(){
cout << "ADT destruct" << endl;
}
};
int main()
{
char *p = new(nothrow) char[sizeof ADT + 1];
if (p == NULL) {
cout << "alloc failed" << endl;
}
ADT *q = new(p) ADT; //placement new:不必担心失败,只要p所指对象的的空间足够ADT创建即可
//delete q;//错误!不能在此处调用delete q;
q->ADT::~ADT();//显示调用析构函数
delete[] p;
return 0;
}
//输出结果:
//ADT construct i=10j=100
//ADT destruct
10.static的用法和作用?
static修饰后的对象与普通对象不同,其不同体现在生命周期以及存储空间。
生命周期:程序编译至程序结束时释放空间为整个生命周期。
存储空间:静态对象存储在静态存储空间里。静态存储空间分为两段:DATA段和BSS段。其中DATA段存放已初始化的值,BSS段反之存放未初始化的值。值得注意的是,静态存储空间也存放全局变量。
static修饰普通变量,在main()函数之前就申请了存储空间。如果有初始值就用初始值初始化它,如果没有初始值系统用默认值初始化它。
static修饰普通函数,表明函数的作用范围,只可在该文件内使用。 普通函数的范围呢?
静态成员引用不需要特定对象。即不需要生成对象就可以访问该成员。但访问前必须在类体外将其初始化。对类的所有对象只有一份拷贝.
注:普通对象在声明对象时并不占有空间,只有在实例化对象的时候才为对象申请分配空间。
class A
{
public:
static int a;
A();
};
a = VAL;
静态成员函数不需要对象就可以被引用,反之非静态成员函数必须有特定对象。1. 不接收this指针,因而只能访问类的static成员变量。2. 不能被virtual修饰,static成员不属于任何对象或实例,所以加上virtual没有任何实际意义;3. 没有this指针,虚函数的实现是为每一个对象分配一个vptr指针,而vptr是通过this指针调用的,所以不能为virtual;虚函数的调用关系,this->vptr->ctable->virtual function
class A
{
public:
static int a;
A();
static int function1(){};//静态成员函数
int function2(){};//非静态成员函数
};
函数体内static变量的作用范围为该函数体,不同于auto变量,该变量的内存只被分配一次,因此其值在下次调用时仍维持上次的值;
static类对象必须要在类外进行初始化,static修饰的变量先于对象存在,所以static修饰的变量要在类外初始化;
1. 隐藏。(static函数,static变量均可)
当同时编译多个文件时,所有未加static前缀的全局变量和函数都具有全局可见性。
2.保持变量内容的持久。存储在静态数据区的变量会在程序刚开始运行时就完成初始化,也是唯一的一次初始化。共有两种变量存储在静态存储区:全局变量和static变量,只不过和全局变量比起来,static可以控制变量的可见范围。
3.static的第三个作用是默认初始化为0(static变量)
其实全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是0x00,某些时候这一特点可以减少程序员的工作量。
11.静态变量什么时候初始化
- 初始化只有一次,但是可以多次赋值,在主程序之前,编译器已经为其分配好了内存
- 在C中,初始化发生在代码执行之前,编译阶段分配好内存之后,就会进行初始化,所以我们看到在C语言中无法使用变量对静态局部变量进行初始化,在程序运行结束,变量所处的全局内存会被全部回收。
- 在C++中,初始化时在执行相关代码时才会进行初始化,主要是由于C++引入对象后,要进行初始化必须执行相应构造函数和析构函数,并非简单地分配内存。所以C++标准定为全局或静态对象是首次用到时才会进行构造,并通过atexit()来管理。在程序结束,按照构造顺序反方向进行逐个析构。所以在C++中是可以使用变量对静态局部变量进行初始化的。