Featured image of post C++语法

C++语法

个人学习C++的笔记

|
4482 字
|

拷贝构造函数

参考文章

csdn:C++拷贝构造函数

概述

拷贝构造函数,又称复制构造函数,是一种特殊的构造函数,它由编译器调用来完成一些基于同一类的其他对象的构造及初始化。

其唯一的形参必须是引用,但并不限制为const,一般普遍的会加上const限制。

调用拷贝构造函数的情形

  1. 一个对象作为函数参数,以值传递的方式传入函数体(函数传参,类类型的值传递)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class Complex
{
};
void Fun(Complex c1)
{
}
 
int main()
{
  Complex c1(1,2);
  Fun(c1); // 这里就调用了默认的拷贝构造函数
}
  1. 一个对象作为函数返回值,以值传递的方式从函数返回;(函数的返回类型是类,从局部对象到临时对象的拷贝构造)
1
2
3
4
5
Complex Fun()
{
  Complex c(10,20);
  return c; // 这里会调用
}
  1. 一个对象用于给另外一个对象进行初始化(常称为赋值初始化);(用已有对象去初始化本类的其他对象)
1
2
3
4
5
6
int main()
{
  Complex c1(1,2);
  Complex c2(c1); // 此处
  Complex c3=c1; // 此处
}

浅拷贝与深拷贝

当对象的成员变量中存在指针变量时,用存在的对象初始化新建对象时指针变量一同初始化,但这时调用一般拷贝构造函数(浅拷贝)会使新对象中的指针指向和初始化对象指针指向一致,那么当用来初始化的对象在释放内存时会释放掉指针指向的内存,而当新创建的对象释放时会出现程序错误,以为这个指针指向的内存被释放了两次。因此我们需要手动提供另一种拷贝构造函数(深拷贝)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class MyClass {
public:
    int* data;
    MyClass(int d) {
        data = new int(d);  // 动态分配内存
    }
    ~MyClass() {
        delete data;  // 释放内存
    }
    MyClass(const MyClass& other) {
        data = new int(*other.data);  // 深拷贝:分配新内存并复制内容
    }
};

int main() {
    MyClass original(10);
    MyClass copy(original);  // 调用深拷贝构造函数
    return 0;
}

虚析构函数

总的来说虚析构函数是为了避免内存泄露,而且是当子类中会有指针成员变量时才会使用得到的。也就说虚析构函数使得在删除指向子类对象的基类指针时可以调用子类的析构函数达到释放子类中堆内存的目的,而防止内存泄露的.

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <iostream>
using namespace std;

class Fish
{
  public:
    Fish()
    {
        cout << "Constructed Fish" << endl;
    }
    // 如果这里不是虚析构函数,那么delete pFish时只会调用基类的析构函数,而不会调用子类的析构函数
    virtual ~Fish() // virtual destructor!
    {
        cout << "Destroyed Fish" << endl;
    }
};

class Tuna : public Fish
{
  public:
    Tuna()
    {
        cout << "Constructed Tuna" << endl;
    }
    ~Tuna()
    {
        cout << "Destroyed Tuna" << endl;
    }
};

void DeleteFishMemory(Fish *pFish)
{
    delete pFish;
}

int main()
{
    cout << "Allocating a Tuna on the free store:" << endl;
    Tuna *pTuna = new Tuna;
    cout << "Deleting the Tuna: " << endl;
    DeleteFishMemory(pTuna);

    cout << "Instantiating a Tuna on the stack:" << endl;
    Tuna myDinner;
    cout << "Automatic destruction as it goes out of scope: " << endl;

    return 0;
}

常量成员函数

常量对象和非常量对象

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>
using namespace std;

class MyClass
{
private:
    int x;

public:
    MyClass(int n)
    {
        x = n;
    }
    void setX(int n) // 非常量成员函数
    {
        x = n;
    }
    int getX() const // 常量成员函数
    {
        return x;
    }
};

int main()
{
    MyClass obj1(10);       // 非常量对象
    const MyClass obj2(20); // 常量对象

    obj1.setX(30);                              // 可以修改obj1的数据成员
    cout << "obj1.x = " << obj1.getX() << endl; // obj1.x = 30

    // obj2.setX(40); // 编译错误,不能修改obj2的数据成员(常量对象不能调用非常量成员函数)
    cout << "obj2.x = " << obj2.getX() << endl; // obj1.x = 20

    return 0;
}

常量成员函数

常量成员函数的特点

  1. 常量成员函数不会修改类的成员函数,即它们是只读的。因此,常量成员函数不能修改类的数据成员,也不能调用非常量成员函数,因为非常量成员函数可能会修改类的数据成员。
  2. 常量成员函数可以被常量对象和非常量对象调用。如果一个对象是常量对象,则只能调用该对象的常量成员函数,而不能调用非常量成员函数。
  3. 常量成员函数可以访问类的所有成员变量和常量成员函数。

常量成员函数的作用是保证类的数据成员不被修改,从而提高程序的安全性和可靠性。 常量成员函数通常用于访问类的数据成员,而不是修改它们。 例如:可以使用常量成员函数来实现类的数据成员的读取操作,而使用非常量成员函数来实现类的数据成员的写入操作。

代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <iostream>

using namespace std;


class Person
{
  private:
    string name;
    int age;

  public:
    Person(string n, int a)
    {
        name = n;
        age = a;
    }
    string getName() const
    {
        return name;
    }
    int getAge() const
    {
        return age;
    }
    void show() const
    {
        cout << "Name: " << name << ", Age: " << age << endl;
    }
};

int main()
{
    Person p("Alice", 20);
    p.show();
    return 0;
}

左值和右值

参考文章

csdn:C++ 左值和右值

左值和右值的定义

左值(loactor value):存储在内存中、可寻址的数据

右值(read value):可以提供数据值的数据(不一定可寻址,例如存储在寄存器中的数据)

右值引用

  • 左值引用无法引用右值;
  • 常量左值引用可以操作右值,但是无法对右值进行修改;
  • 右值引用可以对右值进行修改;
  • 常量右值引用:引用一个右值,并且不可更改。可以常量左值引用代替。
1
2
3
4
5
6
7
    int a = 10;
    int &b = a;  // 左值引用
    // int &c = 10; // 错误,左值引用无法操作右值
    b = 20;
    const int &d = 10; // 常量左值引用可以操作右值
    int &&e = 20;   // 右值引用
    e = 25; // 修改右值

因此c++11中引入右值引用&&。

右值引用使用场景

拷贝构造函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>
using namespace std;

class demo
{
  public:
    // 构造函数
    demo() : num(new int(0))
    {
        cout << "construct!" << endl;
    }
    // 拷贝构造函数(深拷贝)
    demo(const demo &d) : num(new int(*d.num))
    {
        cout << "copy construct!" << endl;
    }
    ~demo()
    {
        cout << "class destruct!" << endl;
    }

  private:
    int *num;
};

demo get_demo()
{
    return demo(); // 返回一个demo对象,是一个右值
}
int main()
{
    demo a (get_demo());         // 拷贝构造
    return 0;
}

输出:

construct!
copy construct!
copy construct!
class destruct!
class destruct!

有些编译器可能会优化,只输出一次拷贝构造函数。

如上所示,demo 类自定义了一个拷贝构造函数。该函数在拷贝 d.num 指针成员时,必须采用深拷贝的方式,即拷贝该指针成员本身的同时,还要拷贝指针指向的内存资源。否则一旦多个对象中的指针成员指向同一块堆空间,这些对象析构时就会对该空间释放多次,这是不允许的。

demo a (get_demo()) 的流程:

  1. 执行 get_demo() 函数,demo()调用构造函数生成一个匿名对象
  2. 执行 return demo() ,调用拷贝构造函数拷贝匿名对象,作为函数get_demo()的返回值(get_demo()执行完毕,匿名对象会被销毁)
  3. 执行 a(get_demo()), 调用拷贝构造函数(此行代码执行完毕,get_demo()的返回值会被析构)
  4. 程序结束前,a被析构。

在这个过程中,底层执行了2次深拷贝。如果指针指向的堆空间较大,会大大降低执行的效率。通过移动构造函数可以解决这个问题。

何时调用拷贝构造函数?(详见:拷贝构造函数

移动构造函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
using namespace std;

class demo
{
  public:
    // 构造函数
    demo() : num(new int(0))
    {
        cout << "construct!" << endl;
    }
    // 拷贝构造函数(深拷贝)
    demo(const demo &d) : num(new int(*d.num))
    {
        cout << "copy construct!" << endl;
    }
    // 移动构造函数
    demo(demo &&d) : num(d.num)
    {
        d.num = nullptr;
        cout << "move construct!" << endl;
    }
    ~demo()
    {
        cout << "class destruct!" << endl;
    }

  private:
    int *num;
};

demo get_demo()
{
    demo temp;   // 创建一个局部对象
    return temp; // 返回局部对象
}

int main()
{
    demo a(get_demo()); // 调用移动构造函数
    return 0;
}

输出:

construct!
move construct!
class destruct!
class destruct!

使用右值引用类型的参数,指针浅拷贝,右值对象指针置为nullptr, 从而,避免拷贝堆空间,完成初始化。

当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。

std::move()可以将左值转换为右值,从而使用移动构造。

1
2
3
4
5
demo get_demo()
{
    demo temp;              // 创建一个局部对象
    return std::move(temp); // 使用 std::move 触发移动构造函数
}

输出是一样的

move函数

参考文章

csdn:C++11中的move函数

智能指针

参考文章

csdn:C++智能指针

智能指针概述

是原始指针的封装,会自动分配内存,不需要担心潜在的内存泄露。

为什么使用智能指针

一句话带过:智能指针就是帮我们C++程序员管理动态分配的内存的,它会帮助我们自动释放new出来的内存,从而避免内存泄漏

下面的内存泄露的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <iostream>
#include <string>
#include <memory>

using namespace std;


// 动态分配内存,没有释放就return
void memoryLeak1() {
	string *str = new string("动态分配内存!");
	return;
}

// 动态分配内存,虽然有些释放内存的代码,但是被半路截胡return了
int memoryLeak2() {
	string *str = new string("内存泄露!");

	// ...此处省略一万行代码

	// 发生某些异常,需要结束函数
	if (1) {
		return -1;
	}
	/
	// 另外,使用try、catch结束函数,也会造成内存泄漏!
	/

	delete str;	// 虽然写了释放内存的代码,但是遭到函数中段返回,使得指针没有得到释放
	return 1;
}


int main(void) {

	memoryLeak1();

	memoryLeak2();

	return 0;
} 

memoryLeak1函数中,new了一个字符串指针,但是没有delete就已经return结束函数了,导致内存没有被释放,内存泄露! memoryLeak2函数中,new了一个字符串指针,虽然在函数末尾有些释放内存的代码delete str,但是在delete之前就已经return了,所以内存也没有被释放,内存泄露!

使用指针,我们没有释放,就会造成内存泄露。但是我们使用普通对象却不会。

而智能指针本质是对一个普通指针的封装,利用有生命周期的对象自动释放的特性,来实现内存的自动管理。

auto_ptr

auto_ptr 是c++ 98定义的智能指针模板,其定义了管理指针的对象,可以将new获得(直接或间接)的地址赋给这种对象。当对象过期时,其析构函数将使用delete来释放内存!

用法: 头文件:#include <memory> 用法: auto_ptr<类型> 变量名(new 类型)

例如:

1
2
3
auto_ptr< string > str(new string(“我要成为大牛~ 变得很牛逼!”));
auto_ptr<vector< int >> av(new vector< int >());
auto_ptr< int > array(new int[10]);

下面的代码使用new创建一个对象,但是不使用delete,就会发生内存泄露。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "iostream"
using namespace std;

class Test
{
  public:
    Test()
    {
        cout << "Test的构造函数..." << endl;
    }
    ~Test()
    {
        cout << "Test的析构函数..." << endl;
    }

    int getDebug()
    {
        return this->debug;
    }

  private:
    int debug = 20;
};

int main(void)
{
    Test *test = new Test;
    cout << test->getDebug() << endl;
    // delete test;

    return 0;
}

输出:

Test的构造函数...

要释放内存,就得手动delete,或者使用智能指针

使用智能指针:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
int main(void)
{

    // Test *test = new Test;
    auto_ptr<Test> test(new Test);

    cout << "test->debug:" << test->getDebug() << endl;
    cout << "(*test).debug:" << (*test).getDebug() << endl;

    return 0;
}

输出:

Test的构造函数...
test->debug:20
(*test).debug:20
Test的析构函数...

智能指针可以像普通指针一样使用,并且会自动释放内存

智能指针有三个常用函数:

  1. get():获取智能指针管理的指针

    1
    2
    3
    4
    5
    
     // 定义智能指针
     auto_ptr<Test> test(new Test);
    
     Test *tmp = test.get();		// 获取指针返回
     cout << "tmp->debug:" << tmp->getDebug() << endl;
    

    但一般不这么使用,因为可以直接使用智能指针操作

  2. release():释放智能指针管理的指针

    1
    2
    3
    4
    5
    
     // 定义智能指针
     auto_ptr<Test> test(new Test);
    
     Test *tmp2 = test.release();	// 取消智能指针对动态内存的托管
     delete tmp2;	// 之前分配的内存需要自己手动释放
    
  3. reset():重置智能指针管理的指针

    1
    2
    3
    4
    5
    6
    
     // 定义智能指针
     auto_ptr<Test> test(new Test);
    
     test.reset();			// 释放掉智能指针托管的指针内存,并将其置NULL
    
     test.reset(new Test());	// 释放掉智能指针托管的指针内存,并将参数指针取代之
    

unique_ptr

c++11使用unique_ptr替代auto_ptr

unique_ptr特性:

  1. 基于排他所有权模式:两个指针不能指向同一个资源
  2. 无法进行左值unique_ptr复制构造,也无法进行左值复制赋值操作,但允许临时右值赋值构造和赋值
  3. 保存指向某个对象的指针,当它本身离开作用域时会自动释放它指向的对象。
  4. 在容器中保存指针是安全的
使用 Hugo 构建
主题 StackJimmy 设计