21.C++与Java的区别?
语言特性:
Java语言给开发人员提供更为简洁的语法;完全面向对象,由于JVM可以安装到任何的操作系统上,所以说它的可移植性很强。
Java语言中没有指针的概念,引入了真正的数组。
C++也可以在其他系统运行,但是需要不同的编码。
Java用接口(interface)技术取代C++程序中的抽象类。接口与抽象类有同样的功能,但是省去了在实现和维护上的复杂性。
垃圾回收:
C++用户析构函数后回收垃圾,Java中内存的分配和回收都是自动进行的。
应用场景:
Java在桌面程序上不如C++实用,C++可以直接编译成exe文件,指针是C++的优势,可以直接对内存的操作,但同时具有危险性。
Java在web应用上具有C++不可比拟的优势,具有丰富多样的框架。
对底层程序的编程以及控制方面的编程,C++很灵活。
22.C++中struct和class的区别
相同点:
两者都拥有成员函数、公有和私有部分。
任何可以使用class完成的工作,同样可以使用struct完成。
不同点:
两者中如果不对成员指定公私有,struct默认是公有的,class默认是私有的。
class默认是private继承,struct默认是public继承。
引申:C++和C的struct区别
C语言中:struct是用户自定义数据类型(UDT);C++中struct是抽象数据类型(ADT),支持成员函数的定义,C++中的struct能继承,能实现多态。
C中struct是没有权限的设置的,而struct中只能是一些变量的集合体,可以封装数据却不可以隐藏数据,而且成员不可以是函数。
C++中,struct增加了访问权限,而且可以和类一样有成员函数,成员默认访问说明符为public。
struct作为类的一种特例是用来自定义数据结构的。一个结构标记声明后,在C中必须在结构标记前加上struct,才能做结构类型名;C++中结构体标记(结构体名)可以直接作为结构体类型名使用,此外结构体struct在C++中被当做类的一种特例。
23.define宏定义和const的区别
编译阶段:
define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。
安全性:
define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般最好加上一个大括号包含住全部的内容,要不然很容易出错。
const常量有数据类型,编译器可以对其进行类型安全检查
内存占用:
define只是将宏名称进行替换,咋内存中会产生多份相同的备份。const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的表达式计算出结果放入常量表。
宏替换发生在编译阶段之前,属于文本插入替换;const作用发生在于编译过程中。
宏不检查类型,const检查数据类型。
宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间。
24.C++中const和static的作用
static
不考虑类的情况:
隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用。
默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区。
静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用。
考虑类的情况:
static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外初始化,初始化时不需要标示为static;可以被非static 成员函数任意访问。
static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问。
const
不考虑类的情况:
const常量在定义时必须初始化,之后无法更改。
const形参可以接受const和非const类型的实参。
考虑类的情况:
const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化。
const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable数据的值。
补充:const修饰变量时也与static有一样的隐藏功能。只能在该文件中使用,其他文件不可以引用声明使用。因此在头文件中声明const变量是没有问题的,因为即使被多个文件包含,链接性都是内部的,不会出现符号冲突。
25.C++的顶层const和底层const
概念区分:
顶层const:指的是const修饰的变量本身是一个常量,无法修改,指的是指针,就是*的右边。
底层const:指的是const修饰的变量所指向的对象是一个常量,指的是所指变量,就是*号的左边。比如:
int a = 10;int* const b1 = &a; //顶层const,b1本身是一个常量
const int* b2 = &a; //底层const,b2本身可变,所指的对象是常量
const int b3 = 20; //顶层const,b3是常量不可变
const int* const b4 = &a; //前一个const为底层,后一个为顶层,b4不可变
const int& b5 = a; //用于声明引用变量,都是底层const
区分作用:
执行对象拷贝时有限制,常量的底层const不能赋值给非常量的底层const。
使用命名的强制类型转换const_cast时,只能改变运算对象的底层const。
int const a 和const int a 均表示定义常量类型a。
const int *a ,其中a为指向int型变量的指针,const在*左侧,表示a指向不可变常量。
int *const a,依旧是指针类型,表示a为指向整型数据的常指针。
26.数组名和指针(这里为指向数组首元素的指针)区别?
二者均可以通过增减偏移量来访问数组中的元素。
数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。
当数组名当作形参传递给调用函数后,就失去了原有的特性,退化成了一般指针,多了自增自减操作,但sizeof运算符不能在得到原数组的大小了。
27.final和override关键字
override:
当在父类中使用了虚函数的时候,可能需要在某个子类中对这个函数进行重写,以下方法均可:
class A
{
virtual void foo();
}
class B: public A
{
void foo(); //OK
virtual void foo(); // OK
void foo() override; //OK
}
如果不使用override,当输入错误时,比如将 void foo()写成了 void f00(),编译器并不会报错,因为编译器不知道你的目的是重写虚函数,而是把它当作一个新的函数。因此对整个程序就会产生恶劣的影响,所以,override的作用就出来了,他指定了子类的这个虚函数是重写的父类的,如果名字不小心打错了,编译器是不能通过的:
class A
{
virtual void foo();
};
class B: public A
{
virtual void f00(); //OK,这个函数是B新增的,不是继承的
virtual void f0o() override; //Error, 加了override之后,这个函数一定是继承自A的,A找不到就报错
};
final
当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后被继承或者重写,编译器会报错,例子如下:
class Base
{
virtual void foo();
};
class A : public Base
{
void foo() final; // foo 被override并且是最后一个override,在其子类中不可以重写
};
class B final : A // 指明B是不可以被继承的
{
void foo() override; // Error: 在A中已经被final了
};
class C : B // Error: B is final
{
};
28.拷贝初始化和直接初始化
当用于类类型对象时,初始化的拷贝形式和直接形式有所不同:直接初始化直接调用月实参匹配的构造函数,拷贝初始化总是调用拷贝构造函数。拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。
string str1("I am a string");//语句1 直接初始化
string str2(str1);//语句2 直接初始化,str1是已经存在的对象,直接调用拷贝构造函数对str2进行初始化
string str3 = "I am a string";//语句3 拷贝初始化,先为字符串”I am a string“创建临时对象,再把临时对象作为参数,使用拷贝构造函数构造str3
string str4 = str1;//语句4 拷贝初始化,这里相当于隐式调用拷贝构造函数,而不是调用赋值运算符函数
为了提高效率,允许编译器跳过创建临时对象这一步,直接调用构造函数构造要创建的对象,这样就完全等价于直接初始化了(语句1和语句3等价),但是需要辨别两种情况。
当拷贝构造函数为private时:语句3和语句4在编译时会报错。
使用explicit修饰构造函数时:如果构造函数存在隐式转换,编译会报错。
29.初始化和赋值的区别
对简单类型来说,初始化和赋值没有什么区别。
对于复杂类型来说,这两的区别很大:
class A{
public:
int num1;
int num2;
public:
A(int a=0, int b=0):num1(a),num2(b){};
A(const A& a){};
//重载 = 号操作符函数
A& operator=(const A& a){
num1 = a.num1 + 1;
num2 = a.num2 + 1;
return *this;
};
};
int main(){
A a(1,1);
A a1 = a; //拷贝初始化操作,调用拷贝构造函数
A b;
b = a;//赋值操作,对象a中,num1 = 1,num2 = 1;对象b中,num1 = 2,num2 = 2
return 0;
}
30.extern“C”的用法
为了能够在C++代码中调用C语言的代码:在程序中加入extern“C”后,相当于告诉编译器这部分代码是C语言写的,因此要按照C元进行编译,而不是C++,以下情况使用该方法:
1.C++代码中调用C语言代码
2.在C++头文件中使用
3.多人协同开发时,可能有人擅长C,有人擅长C++
31.野指针和悬空指针
都是指向无效内存区域(这里指的无效是指“不安全不可控”)的指针,访问行为会导致未定义行为。
野指针:
指的是没被初始化的指针。
int main(void) {
int* p; // 未初始化
std::cout<< *p << std::endl; // 未初始化就被使用
return 0;
}
为了防止出错,对于指针初始化时都是赋值为nullptr,这样在使用时编译器就不会直接报错,产生非法内存访问。
悬空指针:
指针最初指向的内存已经被释放了的一种指针。
int main(void) {
int * p = nullptr;
int* p2 = new int;
p = p2;
delete p2;
}
此时,p和p2就是悬空指针,指向的内存已经被释放。继续使用这两个指针,行为不可预测。需要设置p=p2=nullptr。此时再使用,编译器会直接报错。避免野指针比较简单,但是悬空指针比较麻烦。C++引入了只能指针,C++智能指针的本质就是避免悬空指针的产生。
产生的原因及解决办法:
野指针:指针变量未及时初始化——定义指针变量及时初始化,要么置空。
悬空指针:指针free或delete之后没有及时置空——释放操作后立即置空。
32.C和C++的类型安全
什么是类型安全?
类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没有授权的内存区域。“类型安全”常被用来形容编程语言,其根据在于该门编程语言是否提供保障类型安全的机制;有的时候也用“类型安全”形容某个程序,判别的标准在于该程序是否隐含类型错误。
1.C的类型安全
C只在局部上下文中表现出类型安全,比如试图从一种结构体的指针转换成另外一种结构体的指针时,编译器将会报错,除非使用显示类型转换。
2.C++的类型安全
如果C++使用得当,将比C更有类型安全,C++中提供了一些新的机制保障类型安全:
1. 操作符new返回的指针类型严格与对象匹配,而不是void*。
2.C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;
3.引入const关键字代替#define constants,它是有类型。有作用域的,而#define constants只是简单的文本替换。
4.一些#define宏可以被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全。
5.C++提供了dynamic_cast关键字,使得转换过程更加安全,因为dynamic_cast比static_cast涉及更多具体的类型检查。
33.C++中的重载、重写(覆盖)和隐藏的区别
1.重载
指在同一范围定义中的同名成员函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。重载和函数成员是否为虚函数无关。
class A{
...
virtual int fun();
void fun(int);
void fun(double, double);
static int fun(char);
...
}
2.重写(覆盖)(override)
在派生类中覆盖基类中的同名函数,重写就是重写函数体,要求基类函数必须是虚函数且:与基类的虚函数有相同的参数个数、参数类型以及返回值类型。
//父类
class A{
public:
virtual int fun(int a){}
}
//子类
class B : public A{
public:
//重写,一般加override可以确保是重写父类的函数
virtual int fun(int a) override{}
}
重载和重写的区别:
重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系;
重写要求参数列表相同,重载则要求参数类标不同,返回值不要求;
重写关系中,调用方法根据对象类型决定,重载根据调用实参列表和形参表的对应关系来选择函数体。
3.隐藏(hide)
在某些情况下,派生类的函数屏蔽了基类中的同名函数,包括以下情况:
两个函数参数相同,但是基类函数不是虚函数。隐藏和重写的区别在于基类函数是否是虚函数。
//父类
class A{
public:
void fun(int a){
cout << "A中的fun函数" << endl;
}
};
//子类
class B : public A{
public:
//隐藏父类的fun函数
void fun(int a){
cout << "B中的fun函数" << endl;
}
};
int main(){
B b;
b.fun(2); //调用的是B中的fun函数
b.A::fun(2); //调用A中fun函数
return 0;
}
两个函数参数不同,无论基类函数是不是虚函数,都会被隐藏。和重载的区别在于两个函数不在同一个类中。
34.C++有哪几种构造函数
默认构造函数;
初始化构造函数;
拷贝构造函数;
移动构造函数;
委托构造函数;
转换构造函数;
#include <iostream>
using namespace std;
class Student{
public:
Student(){//默认构造函数,没有参数
this->age = 20;
this->num = 1000;
};
Student(int a, int n):age(a), num(n){}; //初始化构造函数,有参数和参数列表
Student(const Student& s){//拷贝构造函数,这里与编译器生成的一致
this->age = s.age;
this->num = s.num;
};
Student(int r){ //转换构造函数,形参是其他类型变量,且只有一个形参
this->age = r;
this->num = 1002;
};
~Student(){}
public:
int age;
int num;
};
int main(){
Student s1;
Student s2(18,1001);
int a = 10;
Student s3(a);
Student s4(s3);
printf("s1 age:%d, num:%d\n", s1.age, s1.num);
printf("s2 age:%d, num:%d\n", s2.age, s2.num);
printf("s3 age:%d, num:%d\n", s3.age, s3.num);
printf("s2 age:%d, num:%d\n", s4.age, s4.num);
return 0;
}
//运行结果
//s1 age:20, num:1000
//s2 age:18, num:1001
//s3 age:10, num:1002
//s2 age:10, num:1002
默认构造函数和初始化构造函数在定义类的对象,完成对象的初始化工作。
复制构造函数用于复制本类的对象。
转换构造函数用于将其他类型的变量,隐式转换为本类对象。
35.深拷贝和浅拷贝的区别
浅拷贝:
浅拷贝只是拷贝一个指针,并没有新开辟一个地址,拷贝的指针和原来的指针指向同一块地址,如果原来的指针所指向的资源释放了,那么再释放浅拷贝的指针资源就会出现错误。
深拷贝:
深拷贝不仅拷贝值,还开辟出一块新的空间来存放新的值,即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。在自己实现拷贝赋值的时候,如果有指针变量的话是需要自己实现深拷贝的。
#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
从执行结果看,浅拷贝在对象创建时存在风险,即被拷贝的对象析构释放资源之后,拷贝对象析构时会再次释放一个已经释放的资源,深拷贝的结果是两个对象之间没有任何关系,格字成员地址不同。
36.内联函数和宏定义的区别
在使用时,宏只做简单字符串替换(编译前)。而内联函数可以进行参数类型检查(编译时),且具有返回值。
内联函数在编译时直接将函数代码嵌入到目标代码中,省去函数调用的开销来提高执行效率,并且进行参数类型检查,具有返回值,可以实现重载。
宏定义时要注意书写(参数要括起来)否则容易出现歧义,内敛函数不会产生歧义。
内敛函数有类型检测、语法判断等功能,宏没有。
内敛函数使用场景:
使用宏定义的地方都可以使用inline函数。
作为类成员接口函数来读写类的私有成员或者保护成员,会提高效率。
37.public,protected和private访问和集成权限/public/protected/private的区别?
访问权限:
访问权限 | 外部 | 派生类 | 内部 |
public | √ | √ | √ |
protected | × | √ | √ |
private | × | × | √ |
继承权限:
1.派生类继承自基类的成员权限有四种状态:public、protected、private和不可见。
2.派生类对基类成员的访问全新取决于两点:①继承方式②基类成员在基类中的访问权限。
3.派生类对基类成员的访问权限是取以上两点中的更小的访问范围(除了private的继承方式遇到private成员是不可见外),如:
public继承+private成员=private;
private继承+protected=private;
private继承+private=不可见;
38.如何用判断代码大小端存储?
大端存储:数据的高字节存储在底地址中;
小端存储:数据的低字节存储在底地址中。
所以在socket编程中,往往需要将操作系统所用的小端存储IP地址转换为大端存储,这样才能进行网络传输。
方式一:使用强制类型转换
#include <iostream>
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;
}
方式二:union
#include <iostream>
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"<<endl;
else if (value.ch == 0x34)
cout << "little endian"<<endl;
}
39.volatile、mutable和explicit关键字的用法
1.volatile
一种类型修饰符,用它声明的类型变量表示可以被某些编译器位置的因素更改,比如:操作系统、硬件或者其他线程等。遇到这个关键字声明的变量,编译器对访问改变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问。
当要求使用volatile声明的变量的值的时候,系统总是重新从他所在的内存读取数据,即使它前面的指令刚刚从该处读过数据。
volatile定义变量的值是易变的,每次用到这变量的值的时候都要重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。
多线程下的volatile:该关键字的作用是防止优化编译器把变量从内存装入CPU寄存器中。
2.mutable
为了突破const的限制而设置的。有时候我们需要再const函数里面修改一些跟类状态无关的数据成员,那么这个函数就应该被mutable来修饰,并且放在函数后面关键字位置。
class person
{
int m_A;
mutable int m_B;//特殊变量 在常函数里值也可以被修改
public:
void add() const//在函数里不可修改this指针指向的值 常量指针
{
m_A = 10;//错误 不可修改值,this已经被修饰为常量指针
m_B = 20;//正确
}
};
3.explicit
用来修饰类的构造函数,被修饰的构造函数的类,不能发生相应的隐式类型转换,只能以显式的方式进行类型转换。
40.什么情况下会调用拷贝构造函数
用类的一个实例化对象去初始化另一个对象的时候;
函数的参数是类的对象时(非引用传递);
函数的返回值是函数体内局部对象的类的对象时,此时虽然发生NRV优化,但是由于返回方式是值传递,所以会在返回值的地方调用拷贝构造函数。