C++杂记

Posted 追风弧箭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++杂记相关的知识,希望对你有一定的参考价值。

1、C++类的默认拷贝构造函数有什么缺点?

C++默认的拷贝构造函数是进行浅拷贝,也就是说拷贝复制的对象和原来的对象共享一块内存地址,当其中一个对象被销毁,那么另外一个对象所指向的内存就变成了空的了。

class A
    A(int x):size(x),data(new char[x])  

    
    ~A()
        delete []data;
    
public:
    int size;
    char* data;


void main()
    A a1(20);
    a1.data[0] = 'a';
    
        A a2 = a1; //此时调用了默认的拷贝构造函数
        cout << a1.data[0] << "  " << a2.data[0] << endl;
        //a2 局部变量被释放,会调动析构函数,释放内存 ,但是此时a1和a2的data指向的是同一块内存地址
    
    a1.data[0] = 'b'; //此时发生异常,因为内存已经被释放了

如果需要避免上述问题的发生需要自己实现拷贝构造函数。

A::A(const A& other)
    if(*this == other)
        return ;
    
    size = other.size;
    data = new char[size];
    std::copy(other.data,other.data+size,data);

这样就实现了深拷贝。
浅拷贝:仅仅逐个成员拷贝,而不拷贝资源的方式。浅拷贝是指元对象与拷贝对象共用一份实体,仅仅是引用的变量名称不同。
深拷贝:既拷贝成员,又拷贝资源的方式。深拷贝是指源对象与拷贝对象相互独立,其中任何一个对象的改变都不会对另外一个对象造成影响。
深拷贝与浅拷贝:
浅拷贝:默认的复制构造函数只是完成了对象之间的位拷贝,也就是把对象里的值完全复制给另一个对象,如A=B。这时,如果B中有一个成员变量指针已经申请了内存,那A中的那个成员变量也指向同一块内存。
    这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
深拷贝:自定义复制构造函数需要注意,对象之间发生复制,资源重新分配,即A有5个空间,B也应该有5个空间,而不是指向A的5个空间。
拷贝构造函数:是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构建以及初始化,其唯一的参数(对象的引用)是不可变的(const类型)。也就是说,当用一个已经存在的对象为一个新的对象进行赋值时,首先要给新对象的数据成员分配空间资源以及创建新对象,然后用源对象的成员值进行初始化。这个行为必须在对象构造的时候进行,而普通构造函数无法完成这项工作。
在C++中以下几种情况拷贝构造函数会被调用:
- 一个对象以值传递的方式传入函数体。
- 一个对象以值传递的方式从函数中返回。
- 一个对象需要通过另外一个对象进行初始化。

拷贝构造函数的作用就是用来复制对象的,如果不显式声明拷贝构造函数的时候,编译器也会生成一个默认的拷贝构造函数,只执行浅拷贝在一般的情况下,浅拷贝运行的也很好。但是在遇到类有指针数据成员的时候就会出现问题。

2、关于重载、重写、覆盖

重载(overload):在C++程序中,可以将语义、功能类似的几个函数用同一个名字表示,但是参数列表不一致(参数个数、参数类型、参数顺序),即函数重载,有几个特点:
1、相同的作用范围(一般在同一个类中)
2、函数名字相同
3、参数列表不同
4、virtual关键字可有可无
覆盖(override):是指派生类函数覆盖基类函数,特征是:
1、不同的范围(派生类和基类)
2、函数名字相同
3、参数相同
4、基类函数必须由virtual
注:重写基类虚函数的时候,会自动转换这个函数为virtual函数,不管有没有加virtual,因此重写的时候不加virtual也是可以的,不过为了易读性,还是加上比较好。
重写(overwrite):隐藏,是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
1、如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏
2、如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字,此时基类的函数被隐藏。

3、大端字节序和小端字节序

端模式分为:小端字节序和大端字节序,也就是字节在内存中的顺序。
小端字节序:低字节位于内存低地址,高字节位于内存高地址。比如一个int型数据 0x12345678 在内存中的表现如下(假设起始地址为0x0029f500):

0x0029f500 0x78
0x0029f501 0x56
0x0029f502 0x34
0x0029f503 0x12

大端字节序:高字节位于内存低地址,低字节位于内存高地址。相同的数据,内存表现如下:

0x0029f500 0x12
0x0029f501 0x34
0x0029f502 0x56
0x0029f503 0x78

网络字节序:就是大端字节序。规定不同系统间通信一律采用网络字节序。

比如对int类型,大小端字节序的转化可以这样实现:

int transfer(int &a)

    return (0x000000FF & a << 24 |   //左移三个字节
    0x0000FF00 & a << 8 |  //左移1个字节
    0x00FF0000 & a >> 8 |  //右移1个字节
    0xFF000000 & a >> 24); //右移3个字节

判断该机器是采用大端字节序还是小端字节序可以采用union联合体,因为联合体所有变量共用一块内存:

union
char c;
int number;
test;

bool is_big_endin()

/* union实际内存长度是int,占4个字节,而char是一个字节,只会取第一个低地址字节 */
    test.number = 0x01000002;
    return (test.c == 0x01);

4、关于字节对齐。

5、关于函数参数const引用和非const引用

首先关于临时变量,临时变量是编译器在程序需要的时候自动生成的临时性变量,这个变量并不出现在外面的代码中,但是确是实际存在的。而临时变量的生成时机通常是在函数参数传递时发生类型转换,以及函数返回值被创建。

void fun1(short &x)
    std::cout << x << std::endl;


void fun2(const short &x)
    std::cout<< x << std::endl;


void main()

    int a = 10;
    fun1(a);  //编译失败 无法将参数 1 从“int”转换为“short &”
    fun2(a);  //ok, a 是int变量,但是函数参数类型为short型的常量引用,这个时候系统会产生一个临时的short变量

上述两个函数的唯一区别就是函数参数的非const引用和const引用。很多人对于临时变量的理解是, 临时变量是常量,所以不允许复制、改变。所以当用作非常量的时候会报错。但是这样的理解又不够准确,临时变量实际上是可以作为左值的,这点在Thinking in C++的第一卷中的7.3节是有介绍的。所以说如果临时变量是常量,那么又有冲突。所以临时变量不能作为非const引用参数,并不是因为临时变量是常量,而是因为C++编译器的一个关于语法的限制,如果一个参数是以一个非const引用传入,那么C++编译器就有理由相信我们传入的参数是可以在函数中修改的,并且这个修改的变量还是可以在函数外使用的,但是当我们把一个临时变量当成非const引用参数传递进来,则由于临时变量的特殊性,程序员不能操作临时变量,而且临时变量在使用完成后会释放,所以如果我们修改一个临时变量实际上是没有意义的,所以C++编译器加入临时变量是不能作为非const引用的这个语义限制,主要是限制这个非常规用法的潜在错误。
注:非const引用只能指向非const同类型的对象,而const引用可以初始化为不同单相关的类型的对象或者初始化为右值。const引用可以初始化为 “常量、表达式、函数”

int add(int x, int y )
    return x + y;


const int &a = add(1,2); //ok
int &a1 = add(1,2); //error 无法从“int”转换为“int &”

int i=3;
const int &a3=i+5;  //ok
int &a4 = i+5; //error  无法从“int”转换为“int &”

6、以下C代码会输出什么?

int main()

    int arr[] =  6,7,8,9,10 ;
    int *ptr = arr;
    *(ptr++) += 123;
    printf("%d %d\\n", *ptr, *(++ptr));
    /*输出8 8,
    解析: C中printf计算参数是从右到左压栈的,
    *(ptr++) += 123; 等价于 *ptr = *ptr + 123; ptr++;*/

7、关于数组和指针

int a[] = 1,2,3,4,5;
int *ptr = (int*)(&a+1);
cout<< *(a+1) << " , " << *(ptr-1) <<endl;
//输出2,5

数组名本身就是指针,再加个&就变成了双指针,这里的双指针就是指二维数组,加1,就是数组整体加一行,ptr就指向a的第六个元素(即使不存在)。

int b[2][5] = 1,2,3,4,5,6,7,8,9,10;
//那么 int *ptr = (int*)(&b+1) = b[1];

8、C++中有了malloc/free,为什么还需要new/delete?

因为malloc和free是C++/C语言的标准库函数,new/delete是C++的运算符。它们都可以用于申请动态内存和释放内存。对于非内部数据类型的对象而言,只用malloc/free无法满足动态对象的要求。对象在创建时要自动执行构造函数,对象在消亡之前要自动执行析构函数。由于malloc/free是函数而不是运算符,不在编译器的控制权限之内,不能够把执行构造函数和析构函数的任务强加给malloc/free。因此C++语言需要一个能够完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。

9、i++和++i的区别

1、i++返回原来的值,++i返回加1后的值。
2、i++不能作为左值,而++i可以。(左值与右值的根本区别在于是否允许取地址&运算符获得对应的内存地址,一般来讲左值是可以放到赋值符号左边的变量)。
相应的代码实现:

//前缀形式,这里返回的是一个引用形式,就是说函数返回值也可以作为一个左值使用
int& int::operator++()

    //函数本身无参,意味着是在自身空间内增加1的
    *this += 1;
    return *this;


//后缀形式,//函数带参,说明有另外的空间开辟
const int int::operator++(int) 

    //函数带参,说明有另外的空间开辟
    int old_value = *this;
    ++(*this);
    return old_value; //返回的是一个临时变量,临时变量是一个右值

举例:

//举例
int i = 0;
int *p1 = &(++i); //ok
int *p2 = &(i++); //error

++i = 5; //ok
i++ = 6; //error

10、定义一个宏,计算结构体成员偏移量

struct ST
    char a;
    int b;


#define Struct_Offset(Type,Member)  (unsigned int)( &(((Type*)0)->Member))

/*微软的实现
    #ifdef __cplusplus
        #define offsetof(s,m) ((size_t)&reinterpret_cast<char const volatile&>((((s*)0)->m)))
    #else
        #define offsetof(s,m) ((size_t)&(((s*)0)->m))
    #endif
*/

void main()

    //计算b成员的偏移量
    unsigned int offset = (unsigned int)( &(((ST*)0)->b)); //等价于 Struct_Offset(ST,b)

总结:ANSI C标准允许任何为0的常量被强制转化为任何一种指针类型,并且转换结构是一个NULL指针,因此 (((Type*)0)的结果就是一个NULL指针。利用这个NULL指针来访问Type的成员当然是非法的,但是 ( &(((Type*)0)->Member))的意图并不是获取Member字段的内容,而仅仅是计算当前结构体实例的首地址为(((Type*)0)时Member字段的地址。编译器根部就不生成访问Member的代码,而仅仅是根据Type的内存结构布局和结构体实例地址再编译器计算这个地址,这样就完全避免了通过NULL指针访问内存的问题。

11、C++的显示类型转化static_cast、dynamic_cast、const_cast、reinterpret_cast的区别。

//语法,new_type为目标数据类型,expression为原始数据类型变量或者表达式
static_cast<new_type>      (expression)
dynamic_cast<new_type>     (expression) 
const_cast<new_type>       (expression) 
reinterpret_cast<new_type> (expression)

static_cast(编译期类型检查、转换):
- 用于基本数据类型之间的转换,如int转换成char
- 把void指针转换成目标类型的指针(不安全!!)
- 把任何类型的表达式转换成void类型。
- 用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换(把子类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的。

// 1. 类转换
class Base ;
class Child : public Base ;

Child *a = new Child();
Base *b = static_cast<Base *>(a);

// 2.基本类型转换
int i = 5;
double r = static_cast<double>(i);

// 3. void* 类型转换
void *p = &i;
int *s = static_cast<int *>(p);

dynamic_cast(运行期类型检查、转换):

dynamic_cast<type*>(e)
dynamic_cast<type&>(e)
dynamic_cast<type&&>(e)

type必须是一个类类型,在第一种形式中,type必须是一个有效的指针,在第二种形式中,type必须是一个左值,在第三种形式中,type必须是一个右值。在上面所有形式中,e的类型必须符合以下三个条件中的任何一个:e的类型是是目标类型type的公有派生类、e的类型是目标type的共有基类或者e的类型就是目标type的的类型。如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0。如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个std::bad_cast异常(该异常定义在typeinfo标准库头文件中)。e也可以是一个空指针,结果是所需类型的空指针。
dynamic_cast主要用于类层次间的上行转换和下行转换,还可以用于类之间的交叉转换(cross cast)。
在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的;
在进行下行转换时,dynamic_cast具有类型检查的功能,比static_cast更安全。dynamic_cast是唯一无法由旧式语法执行的动作,也是唯一可能耗费重大运行成本的转型动作。
源类中必须要有虚函数,保证多态,才能使用dynamic_cast(expression),在多态类型之间的转换主要使用dynamic_cast,因为类型提供了运行时信息。

const_cast:
const_cast,用于修改类型的const或volatile属性。
该运算符用来修改类型的const(唯一有此能力的C++-style转型操作符)或volatile属性。除了const 或volatile修饰之外, new_type和expression的类型是一样的。
①常量指针被转化成非常量的指针,并且仍然指向原来的对象;
②常量引用被转换成非常量的引用,并且仍然指向原来的对象;
③const_cast一般用于修改底指针。如const char *p形式。

const int g = 20;
int *h = const_cast<int*>(&g);//去掉const常量const属性

const int g = 20;
int &h = const_cast<int &>(g);//去掉const引用const属性

 const char *g = "hello";
char *h = const_cast<char *>(g);//去掉const指针const属性

reinterpret_cast
type-id 必须是一个指针、引用、算术类型、函数指针或者成员指针。它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,再把该整数转换成原类型的指针,还可以得到原先的指针值)。

以上是关于C++杂记的主要内容,如果未能解决你的问题,请参考以下文章

C++中用new开辟一个对象后怎么给其私有数据成员赋值

C++杂记

C++杂记

杂记6--一些c++踩过的坑

(C++ 杂记) —— 自己编写一个math类

(Visual Studio 杂记) )—— Visual Studio 如何 设置 C++ 标准版本