C语言到C++快速入门

发布于:2024-04-27 ⋅ 阅读:(30) ⋅ 点赞:(0)

前言:

通过前面的学习,我们了解了C语言的一些性质和用法,为了更加深入的学习C,我们可以向C++进阶,探究C++的知识世界,相信可以收获不少知识! 

一.C语言和C++的关系:

  1. 起源与发展:C语言是由Dennis Ritchie在1970年代初期开发的,它最初是为了重新设计UNIX操作系统而创建的。C++则是在C语言的基础上发展而来的,由Bjarne Stroustrup在1980年代初期开始设计,其目标是增强C语言的功能,特别是支持面向对象编程
  2. 语法与特性:C++的语法大部分与C语言相似,这使得熟悉C语言的程序员可以很容易地理解C++代码。然而,C++增加了许多新特性,如类、模板、异常处理等,这使得C++比C语言更加强大和灵活。
  3. 编程范式:C语言主要支持过程式编程,而C++则支持多种编程范式,包括过程式、面向对象和泛型编程。这使得C++能够处理更复杂的任务,并在需要时提供更高级别的抽象。
  4. 性能:由于C++和C语言在底层都提供了对内存和硬件的直接访问,因此它们通常能够生成高效的代码。然而,C++的某些特性(如异常处理和模板元编程)可能会在某些情况下引入额外的开销。
  5. 应用领域:C语言在系统级编程(如操作系统、编译器和嵌入式系统)中非常流行,因为它提供了对硬件的直接访问能力。C++则广泛应用于各种领域,包括游戏开发、桌面应用、服务器软件等,特别是那些需要面向对象特性和高级抽象的任务。
  6. 兼容性:C++是C的超集,这意味着大多数有效的C代码也是有效的C++代码。然而,并不是所有的C++代码都可以在C编译器中编译,因为C++添加了许多C没有的特性。

二.基本头文件的变化:

C语言 C++(在C++中使用C语言风格时)
stdio.h cstdio
stdlib.h cstdlib
string.h cstring
math.h cmath
errno.h(注意这里都用的一样) errno.h

*具体在C++中使用的头文件视情况而定。

三.常用指令:

  1. 包含头文件(必须):用于包含其他文件中声明的函数、类、变量等。例如:
    #include <iostream>

  2. 命名空间(必须):用于引入命名空间中的符号,使其可在当前作用域内直接访问。例如:

    using namespace std;
  3. 预处理(可选):用于在编译之前进行文本替换、条件编译等操作。例如:

    #define MAX_SIZE 100
  4. 类型定义(可选):用于定义新的数据类型,通常使用 typedefusing 关键字。例如:

    typedef int Length;
  5. 模板声明(常用):用于声明模板类或函数。例如:

    template <typename T> class MyTemplate;
  6. 类和函数声明(可选):用于声明类、函数、变量等。例如:

    class MyClass; void myFunction(); extern int globalVariable;
  7. 访问修饰符(可选):用于控制类成员的访问权限,包括 publicprotectedprivate。例如:

    class MyClass { public: // 公有成员 private: // 私有成员 protected: // 受保护的成员 };
  8. 内联函数(常用):用于声明内联函数,以便编译器在调用处直接展开函数体,提高执行效率。例如:

    inline int add(int a, int b) { return a + b; }
  9. 异常处理(可选):用于异常处理,包括 trycatchthrow。例如:

    try { // 可能会抛出异常的代码块 } catch (const std::exception& e) { // 异常处理代码 }

四.C++的入门学习:

1.输入输出:(cin、cout、endl)

1.在C++中,cout<<"a\n";和cout<<"a"<<endl;是否有区别?

答:有,一般使用cout << "a" << endl; 更标准。

在 C++ 中,cout << "a\n"; 和 cout << "a" << endl; 这两行代码在大多数情况下在输出上看起来是一样的,它们都会打印字符 'a' 和一个换行符,然后刷新输出缓冲区。然而,它们在底层行为上有所不同。

  1. cout << "a\n";
    这一行直接输出字符串 "a" 和一个换行符(\n)。换行符会被输出到标准输出流(通常是控制台),但是否立即刷新输出缓冲区取决于具体的实现和设置。在大多数情况下,输出缓冲区会在遇到换行符时自动刷新,但这并不是 C++ 标准所保证的。

  2. cout << "a" << endl;
    这一行首先输出字符串 "a",然后输出 endlendl 是一个操作符,它输出一个换行符并立即刷新输出缓冲区。因此,使用 endl 可以确保在输出换行符后立即将任何挂起的输出发送到其目标(如控制台)。

在大多数情况下,这两个语句在输出上的效果是相同的,因为输出缓冲区通常会在遇到换行符时自动刷新。但是,在某些特定情况或系统配置下,可能会看到不同的行为。特别是当输出被重定向到文件或其他非交互式设备时,输出缓冲区的行为可能会有所不同。

因此,如果你需要确保输出立即发送到其目标(例如,在调试或日志记录时),那么使用 endl 可能是更好的选择如果你不关心输出是否立即发送,或者你知道输出缓冲区会自动刷新(例如在大多数控制台应用程序中),那么使用 \n 就足够了。

2.引用变量:(&)

介绍:

引用变量在C++中是一个独特的特性,而在C语言中并不存在。引用是由C++引入的,它为程序员提供了一种更加灵活和方便的方式来处理变量和函数参数。在C语言中,如果要达到类似的效果,通常需要使用指针来实现,但指针语法相对较为复杂,并且容易引起一些错误,如空指针引用和内存泄漏等。引用变量的引入使得C++在处理函数参数传递、函数返回值和对象操作时更加方便和高效

特性:

别名:引用变量提供了对另一个变量的别名,允许通过不同的名称访问同一内存位置的数据,从而简化了代码并提高了可读性。

初始化:引用变量必须在声明时初始化,并且一旦初始化后,就无法再绑定到其他变量。

例如:

int x = 10; int& ref = x; // 引用变量ref初始化为变量x的引用

▲与指针的区别:引用变量类似于指针,但有一些重要区别。指针可以重新赋值和指向其他变量,而引用变量一旦初始化后就无法改变其绑定的对象。此外,引用变量不需要使用解引用操作符(*)来访问其绑定的对象。

▲函数参数传递:引用变量可以用作函数参数,并且允许函数修改原始数据
例如:

//交换a,b的值,在C语言中需要使用指针
void swap2(int &a,int &b)
{
	int t;
	t=a;a=b;b=t;
}
int main()
{
	int a=1;
	int b=20;
	swap2(a,b);
	cout<<a<<" "<<b;
}

函数返回值函数可以返回引用,允许返回对局部变量的引用,但要确保返回引用的对象在函数调用结束后仍然有效。

避免空引用:引用变量不允许为空,因此避免了空指针引用的问题,提高了程序的安全性。

重载运算符:引用变量在重载运算符时非常有用,使得自定义类型的操作更加简洁和易读。

3.默认形参和函数重载: 

//默认参数
int add(int a,int b=2)
{    
    return a+b;
}
int main()
{
    add(1);//返回3
    add(1,3);//返回4
    return 0;
}

默认形参可以在函数传参时提供一个默认值。当调用函数时,如果没有为这些参数提供实参,编译器会自动使用默认值。这在你不希望每次都为所有参数提供值时非常有用,特别是当某些参数通常使用相同的值时。

▲注意,没有默认形参的参数一定要放在有默认形参的前面,这样才能使编译器正确识别。

//函数重载
int add(int a,int b)
{
    return a+b;
}
double add(double a,double b)
{
    return a+b;
}

当我们使用相同的函数名定义一个函数时,通常是为了方便输入数据类型不同时做相同的运算。例如我们需要将两个int型或者是double类型的数相加,编译器会根据函数调用的参数列表和类型来决定调用哪个重载版本的函数,这样就不需要考虑不同函数名的问题了。

4.函数模版 :(template)

template<typename T>
T add(T a,T b)
{
    return a+b;
}

通过定义一个与类型无关的通用函数,并在使用时通过类型参数进行实例化,从而实现了更高级别的代码重用。这样可以快速使用函数,大大减少需要为不同数据类型编写重复代码的情况(一些函数重载),提高了代码重用性和简单性。

#include<iostream>
#include<string>
using namespace std;
template<typename T>
T add(T y,T x)
{
	return y+x;
}
int main()
{
	cout<<add<int>(1,2)<<endl;//3
	cout<<add<double>(1.4,2.5)<<endl;//3.9
	cout<<add<string>("dadada","lalala")<<endl;//dadadalalala
	return 0;
}

升级一点的玩法:

#include<iostream>
using namespace std;
template<typename T,typename t>
auto add(T y,t x)->decltype(y+x)//decltype(尾随返回类型)用来推导计算后的类型,并作为返回类型,auto是自动的意思
{
	return y*x;
}
int main()
{
	cout<<add(0.1,3)<<endl;
	return 0;
}

这段代码可以实现不同类型的数相加相乘的简易代码(一般会涉及浮点变换的问题),auto+decltype会自动将计算后的结果类型作为函数的返回类型,前提是计算结果要为一个正常的数。

5.字符串 :(string)

 初始化:

string s="hello";//赋值运算初始化
string s1="hello";//构造函数初始化
两个初始化都可以

常用的string类中的成员函数:(头文件<string>)

substr() - 复制子串

string s = "Hello,world!";
string sub = s.substr(6, 5);
// 从索引7开始,长度为5的子串 -> "world"

append() - 追加字符串

string s = "Hello";
s.append(", world!"); // s 现在为 "Hello, world!"
insert() - 插入字符串
string s = "Hello";  
s.insert(5, " world"); // 在索引5的位置插入" world" -> "Hello world"

erase() - 删除子串

string s = "Hello world";  
s.erase(6, 5); // 从索引6开始,删除5个字符 -> "Hello"
replace() - 替换子串
string s = "Hello world";  
s.replace(6, 5, "everyone"); // 从索引6开始,替换5个字符为"everyone" -> "Hello everyone"
find() - 查找子串或字符
string s = "Hello world";  
size_t pos = s.find("world"); // 查找"world"的位置 -> 6
find_first_of() - 查找任何指定字符首次在主串出现的位置
string s = "Hello world";  
size_t pos = s.find_first_of("aeiou"); // 查找e字母的位置 -> 1(在主串中的位置)
size() 或 length() - 获取字符串长度
string s = "Hello";  
size_t len = s.size(); // 或 s.length(); -> 5
empty() - 检查字符串是否为空
string s = "";  
bool isEmpty = s.empty(); // 如果字符串为空,则返回true
compare() - 比较字符串
string s1 = "apple";  
string s2 = "banana";  
int cmp = s1.compare(s2);
/*
compare 方法从 s1 和 s2 的第一个字符开始比较。
如果找到不同的字符,并且 s1 的字符ASCII码值大于 s2 的字符ASCII码值,则返回一个正数;如果小于,则返回一个负数。
如果在比较过程中到达任一字符串的末尾而仍未找到不同的字符,则较短的字符串被认为在字典顺序上位于较长的字符串之前(除非两个字符串长度相同且完全相等)。
如果两个字符串完全相等(即长度相同且每个对应位置的字符都相同),则 compare 方法返回0。
*/
swap() - 交换两个字符串的内容
string s1 = "Hello";  
string s2 = "world";  
s1.swap(s2); // s1现在是"world",s2现在是"Hello"
clear() - 清除字符串内容
string s = "Hello";  
s.clear(); // s现在为空字符串

6.升级版动态数组:(vector容器)

<vector> 是 C++ 标准模板库(STL:STL提供了迭代器,用于在容器和算法之间建立联系,使得算法可以访问和操作容器中的数据。迭代器提供了一种抽象的方式来遍历和操作容器中的元素,使得STL算法更加灵活和通用。)中的一个头文件,它定义了一个名为 std::vector 的模板类。std::vector 是一个动态数组,可以容纳多个相同类型的元素,并且能够根据需要自动增长或缩小其大小。

主要特点是:

动态大小std::vector 可以动态地调整其大小,以适应存储更多或更少的元素。当添加新元素而空间不足时,它会自动分配更多的内存。同样,当删除元素并且空间过多时,它可能会释放一些内存。因此它非常适合用于那些大小在运行时才确定的数据结构。

连续存储std::vector 中的元素在内存中连续存储,这意味着可以通过简单的指针运算或迭代器操作来访问元素。

随机访问迭代器std::vector 提供随机访问迭代器,允许在常数时间内访问任何元素。

类型安全std::vector 是模板类,这意味着它可以用于存储任何类型的元素,同时保证类型安全。

#include<iostream>
#include<vector>
using namespace std;
int main()
{
	vector<int> a={1,2,3};
	a.push_back(4);
	a.push_back(5);
	for(const auto& b:a)//const表示允许只读操作,auto表示自动判定b的类型,&为引用。整体是一个增强型for循环。
	{
		cout<<b<<endl;
	}
	cout<<endl;
	a.pop_back();
	for(const auto& b:a)
	{
		cout<<b<<endl;
	}
	return 0;
}

输出: 

1
2
3
4
5

1
2
3
4

*7.容器和迭代器: (拓展自6)

容器: 

在编程和数据结构中,容器指的是一种能够存储和管理一组元素的数据结构。这些元素可以是基本数据类型,如整数或浮点数,也可以是复杂的数据类型,如对象或自定义结构体。容器不仅负责存储元素,还提供了许多方法来访问、修改和遍历这些元素。

以C++的STL(标准模板库)为例,vectorsetlistmap等都是容器的具体实现。这些容器各自有其特点和用途:

  • vector:一个动态数组,可以存储相同类型的元素,并允许通过索引快速访问任意位置的元素。它会自动管理数组大小,不需要手动开辟空间和释放空间。

  • set:一个基于红黑树实现的集合包含唯一元素的序列。它自动对元素进行排序,并提供了快速的插入、删除和查找操作。

  • list:一个双向链表,包含元素的序列。与vector不同,list在插入和删除元素时具有更高的效率,但访问特定位置的元素则相对较慢。

  • map:一个关联容器存储的元素是键值对。它根据键对元素进行排序,并允许通过键快速访问对应的值。

容器的概念不仅存在于C++中,在其他编程语言和数据结构库中也有类似的概念。容器的使用使得程序员能够更加方便地管理和操作数据集合,提高了代码的效率和可维护性。
 

容器的特点:

  • 存储能力:容器能够存储一定数量的元素,这些元素可以是基本数据类型或自定义类型。

  • 访问和修改:容器提供了各种方法来访问和修改存储的元素,例如通过索引访问、迭代器遍历、插入新元素、删除元素等。

  • 内存管理:容器负责自动管理其内部元素的内存分配和释放,使得程序员无需关心这些细节。

迭代器:

在C++中,迭代器(Iterator)是一种对象,它允许你在容器(比如数组、向量、集合、映射等)中遍历元素序列,并且可以访问这些元素。迭代器是这些容器提供的一种机制,用于遍历容器中的元素,它封装了对容器中元素的访问,使得我们可以通过迭代器来遍历、读取或修改容器中的元素。迭代器提供了一种间接访问容器中元素的方式,而不是直接访问元素的内存地址。

我们按照迭代器功能特性分:

  1. 输入迭代器(Input Iterators):这是最基本的迭代器类型,它只能用于读取容器中的元素。输入迭代器只能向前移动,且不能递减。例如,std::istream_iterator就是一种输入迭代器,它用于从输入流中读取数据。
  2. 前向迭代器(Forward Iterators):前向迭代器是输入迭代器的超集,它除了可以读取元素外,还支持多次通过同一元素。也就是说,你可以保存一个前向迭代器的副本,稍后再使用它来访问相同的元素。例如,std::vectorstd::list的迭代器就是前向迭代器。
  3. 双向迭代器(Bidirectional Iterators):双向迭代器是前向迭代器的超集,它除了可以向前移动外,还可以向后移动。这意味着你可以使用双向迭代器来遍历容器中的元素,并可以从后往前遍历。例如,std::list、std::map和std::set的迭代器就是双向迭代器。
  4. 随机访问迭代器(Random Access Iterators):随机访问迭代器是双向迭代器的超集,它提供了最强大的功能。除了可以向前和向后移动外,随机访问迭代器还支持在常数时间内跳到容器中的任何位置。你可以使用加减运算符来移动迭代器,也可以使用比较运算符来比较两个迭代器。例如,std::vectorstd::arraystd::string的迭代器就是随机访问迭代器。

▲需要注意的是,虽然这些迭代器类型在功能上有所不同,但它们之间并不是完全独立的。

在C++ STL中的vectorsetlistmap这四种容器中,迭代器的使用方式确实有一些不同之处。这主要是因为这些容器底层的数据结构不同,导致了迭代器在遍历和操作元素时的行为也有所不同。

  1. vector迭代器
    vector是一个动态数组,因此其迭代器支持像普通指针一样的加减运算,可以直接通过迭代器加上或减去一个整数来访问向量中的其他元素。此外,vector的迭代器支持随机访问迭代器(Random Access Iterator)的所有操作,包括使用下标操作符[]和比较运算符(如<<=>>===!=)等。

  2. set迭代器
    set是一个基于红黑树实现的集合,因此其迭代器是双向迭代器(Bidirectional Iterator),不支持像vector迭代器那样的随机访问。这意味着你不能通过迭代器加减整数来直接访问集合中的其他元素。但是,你可以使用迭代器的前置和后置递增(++it 和 it++)以及前置和后置递减(--it 和 it--)来遍历集合中的元素。

  3. list迭代器
    list是一个双向链表,因此其迭代器的行为和set迭代器类似,也是双向迭代器。你不能通过迭代器进行随机访问,但可以通过递增和递减操作来遍历链表中的元素。

  4. map迭代器
    map是一个关联容器,存储的元素是键值对。map的迭代器允许你访问每个键值对。与setlist一样,map的迭代器也是双向迭代器,不支持随机访问。你可以使用迭代器来遍历map中的元素,并通过解引用迭代器来访问每个元素的键和值。

8.动态内存分配:(new and delete)

C语言 C++
使用头文件stdlib.h中的malloc分配 使用new创建动态内存对象(和java中的创建对象有点相似)
int* arr = (int*)malloc(n * sizeof(int)); int* arr=new arr[n];
free() delete\delete[]

拓展补充:

那么c++这种创建一个int类型对象的方式和java中创建对象的方式是不是有异曲同工之妙呢?

 答:

  1. 内存管理
    • C++:在C++中,使用new操作符在堆上分配内存,并返回指向该内存的指针。程序员需要显式地使用deletedelete[]来释放内存。如果忘记释放内存,可能会导致内存泄漏。
    • Java:在Java中,使用new关键字创建对象时,也会在堆上分配内存。但Java提供了垃圾回收机制,自动管理内存,程序员不需要(也不能)显式地释放内存。
  2. 初始化
    • C++:当使用new创建对象时,C++会调用对象的构造函数(如果有的话)来进行初始化。
    • Java:同样,Java在创建对象时也会调用构造函数进行初始化。
  3. 类型安全
    • C++:C++的类型系统允许进行更底层的操作,包括类型转换和指针运算,这可能导致类型安全问题。
    • Java:Java的类型系统更加严格,提供了更好的类型安全性,并且不支持指针运算。(但是java中提供了类似指针的方法:引用,数组或者Java NIO 等)
  4. 异常处理
    • C++:在C++中,如果new操作失败(例如,由于内存不足),它会返回nullptr。程序员需要显式地检查这个指针,并处理可能的异常
    • Java:在Java中,如果内存分配失败,会抛出一个OutOfMemoryError异常,这通常是一个无法恢复的错误。
  5. 对象的生命周期
    • C++:在C++中,对象的生命周期完全由程序员控制,包括其创建和销毁。
    • Java:在Java中,对象的生命周期由Java虚拟机(JVM)的垃圾回收器管理。当对象不再被引用时,垃圾回收器会自动回收其占用的内存。

9.文件IO流:

 C语言:

#include <stdio.h>
int main() 
{
	// 打开文件以供写入
	FILE *f = fopen("text.txt", "w");//看拓展
	if (f == NULL) 
	{
		printf("无法打开文件\n");
		return 1;
	}
	// 写入数据到文件
	fprintf(f, "%.2f %s", 3.14, "hello world!");
	// 关闭文件
	fclose(f);
	// 打开文件以供读取
	f = fopen("text.txt", "r");
	if (f == NULL) 
	{
		printf("无法打开文件\n");
		return 1;
	}
	double a;
	char b[50];
	// 从文件中读取数据
	fscanf(f, "%lf %[^\n]", &a, b);
	// 输出读取的数据
	printf("%.2f %s\n", a, b);
	// 关闭文件
	fclose(f);
	return 0;
}

拓展:

C语言中的文件打开模式:(FILE *f = fopen("text.txt", "w");)

  • "r":只读模式。打开文件用于读取,文件必须存在,否则返回 NULL。
  • "w":写入模式。创建一个新文件用于写入,如果文件已经存在,则清空文件内容
  • "a":追加模式。打开文件用于写入,在文件末尾追加内容,如果文件不存在则创建新文件。
  • "r+":读写模式。打开文件用于读写操作,文件必须存在。
  • "w+":读写模式。创建一个新文件用于读写操作,如果文件已经存在,则清空文件内容。
  • "a+":读写模式。打开文件用于读写操作,在文件末尾追加内容,如果文件不存在则创建新文件

C++:

#include<iostream>
#include<fstream>
using namespace std;
int main()
{
	ofstream f("text.txt");
	f<<3.14<<" "<<"hello world!";
	f.close();
	ifstream f1("text.txt");
	double a;
	string b;
	f1>>a;
	getline(f1,b);
	cout<<a<<" "<<b;
	f.close();
}

总结:相比较之下,我们发现C++的代码更加简洁明了,因为C++头文件#include<fstream>中封装了文件操作的更多细节,这也可以帮助我们使用更少的代码表示相同的操作。

10.输入输出的重载:(+-*/<<>>等) 

代码以>>和<<为例子: 

#include <iostream>
#include <vector>
#include <string>
using namespace std;
class student //默认为私有类,封装
{
	string name;
	double core;
	friend istream& operator>>(istream &i,student &stu);//声明operator>>作为友元函数
	friend ostream& operator<<(ostream &o,const student &stu);
	
};
istream& operator>>(istream &i,student &stu)
{
	cout<<"请输入姓名:";
	i>>stu.name;
	cout<<"请输入分数:";
	i>>stu.core;
	return i;
}
ostream& operator<<(ostream &o,const student &stu)
{
	o<<"姓名:"<<stu.name<<" 分数:"<<stu.core<<endl;
	return o;
}
int main() 
{
	int n;
	cin>>n;//这里int(int、double、char等)类型为内置类型,使用默认运算符功能
	student stu;
	vector<student> a;
	for(int i=0;i<n;i++)
	{
		cin>>stu;//重载>>,stu是自定义类型,使用的是重载的运算符功能,即使用自定义的拓展功能
		a.push_back(stu);
	}
	for(const auto& b:a)
	{
		cout<<b;//重载<<
	}
	return 0;
}

输入: 
3
请输入姓名:li
请输入分数:90
请输入姓名:hua
请输入分数:89
请输入姓名:pin
请输入分数:97

输出:
姓名:li 分数:90
姓名:hua 分数:89
姓名:pin 分数:97

代码以+为例:

#include<iostream>
using namespace std;
class Point
{
private:
	double x,y;
public:
	Point(double x1=0.0,double y1=0.0):x(x1),y(y1){}//构造函数,将x1的值赋给x,y同理
	Point operator+(const Point& a)//重载+,使对象可以相加
	{
		return Point(this->x+a.x,this->y+a.y);
	}
	void print()//输出
	{
		cout<<"("<<x<<","<<y<<")"<<endl;
	}
};
int main()
{
	Point p2(34,74);
	Point p1(2,4);
	Point p3=p1+p2;//相加时,p1可以看做当前类的对象,p2作为参数传递给函数,返回的对象赋值到p3
	p3.print();
	return 0;
}

输出: (36,78)

重载运算符的优缺点:

优点:代码直观明了,可以自定义运算符的拓展功能;使得部分代码更加简洁,一次重载后全局有效,只要声明了友元函数。

缺点: 可能引发混淆,如果不恰当地重载运算符,可能会导致代码难以理解和维护;并不是所有运算符都能重载,也不能创建新的运算符(如@);另外编译器解析重载时会有额外性能开销。

通常使用在: 

 自定义数据类型:如结构体,类等;数学物理模拟计算;容器和迭代器使用时;以及其他需要使用到运算符默认功能外的扩展功能的地方

▲重载位置: 

作为成员函数重载在结构体中:+一元加号运算符-(一元减号运算符 、[](下标运算符)、=(赋值运算符)

作为外部函数(非成员函数):+-、*、/(二元运算符加减乘除,需要两个参数时 、<< 和 >>(一般作为外部函数)

11. string类复现:(重载+拷贝+析构)

拷贝构造函数: 

拷贝构造函数是一个特殊的构造函数,它用于创建一个新对象作为现有对象的副本。因此副本和原件指向同一块地址,修改其中一个时,另一个也将被修改。

拷贝构造函数在以下情况下被调用:

  1. 当一个对象以值传递的方式传递给函数时
  2. 当一个对象从函数返回时(返回值优化可能改变这一行为)
  3. 当一个对象被初始化为另一个对象的副本时(如className obj2 = obj1;
  4. 当使用数组初始化列表时
  5. 当编译器生成临时对象时(例如,在某些表达式求值中)

▲重载拷贝函数的主要作用:

1.深拷贝与浅拷贝的控制:默认情况下,编译器会提供一个默认的拷贝构造函数,它执行浅拷贝(shallow copy),即仅复制对象的指针或引用,而不是它们指向的数据。这可能导致问题,特别是当对象拥有动态分配的内存或其他资源时。通过重载拷贝构造函数,你可以实现深拷贝(deep copy),即创建资源的新副本,从而避免资源共享和潜在的悬挂指针等问题

2.资源管理的定制:在某些情况下,对象的拷贝可能需要特殊的资源管理。例如,一个对象可能包含文件句柄、网络连接或锁等,这些都需要在拷贝过程中妥善处理。重载拷贝构造函数允许你定义这些资源的拷贝行为。

析构函数: 

析构函数是一种特殊的成员函数,它在对象的生命周期结束时自动被调用。析构函数用于执行对象销毁前的清理操作,比如释放动态分配的内存、关闭打开的文件、断开网络连接等析构函数的名字与类名相同,但在前面加上一个波浪号(~)。析构函数没有返回类型,也没有参数。 

▲覆盖析构函数的作用;

在析构函数体中添加日志输出或调试信息,在析构函数被自动调用时,我们可以更加方便的了解内存的清理释放

#include<iostream>
using namespace std;
class String
{
	char* data;
	int n;
public:
#if 0
	String(const String &s)//这里重载了拷贝函数,使每一个String类型的对象都有一块独立的内存
	{
		cout<<"执行了拷贝构造函数";
		data=new char[s.n+1];
		n=s.n;
		for(int i=0;i<n;i++)
		{
			data[i]=s.data[i];
		}
		data[n]='\0';
	}  
#endif 
	String(const char *a=nullptr)//构造函数,初始化对象
	{
		if(a==0)
		{
			data=0;n=0;
			return;
		}
		const char* p=a;
		while(*p)p++;
		n=p-a;
		data=new char[n+1];
		for(int i=0;i<n;i++)//这里复制数组到data,方便以下的重载和size返回
		{
			data[i]=a[i];
		}
		data[n]='\0';
	}
	char& operator[](int i)//重载[]
	{
		if(i<0||i>=n)throw "下标异常!";
		return data[i];
	}
	int size()const//返回字符串大小
	{
		return n;
	}
};
ostream& operator<<(ostream& o,String a)
{
	for(int i=0;i<a.size();i++)
	{
		o<<a[i];
	}
	o<<endl;
	return o;
}
int main()
{
	String str("hello world!");
	cout<<str;
	
	String str1=str;
	str1[1]='E';
	cout<<str1;
	cout<<str;//这里发现str的值随str1发生了改变,这里涉及拷贝函数
	return 0;
}

 对于拷贝构造函数: 

输出:(未执行重载的拷贝构造函数)

hello world!
hEllo world!
hEllo world!

#if 0

当我们没有重载拷贝构造函数时,类的对象会指向同一块地址,导致改动一个对象的值其他对象的值也发生变化,这是因为String str1=str时,会将同一个地址传给str1,也可以理解为指针指向同一地址。

这里的指针是指向String对象的地址的,而不是数据,拷贝也是拷贝的地址,不是数据。

例如:String str2=str1;
           str1和str2指向同一地址

#if 1

当执行拷贝构造函数重载时,每一个String类对象会创建自己的数据副本,意思是他们会相互独立,不再会因为指针指向同一地址导致改动某一对象中的数据使另外一个对象的数据发生变化,保证了对象的独立性。

输出:(执行了重载的拷贝构造函数)

执行了拷贝构造函数hello world!
执行了拷贝构造函数执行了拷贝构造函数hEllo world!
执行了拷贝构造函数hello world!

添加析构函数的分析:

#include<iostream>
using namespace std;
class String
{
	char* data;
	int n;
public:
	~String()//覆盖了原始析构函数,添加输出便于观察
	{
		cout<<"执行了析构函数 "<<endl;
		delete[] data;
	}
#if 1
	String(const String &s)//重载拷贝构造函数
	{
		cout<<"执行了拷贝函数 ";
		data=new char[s.n+1];
		n=s.n;
		for(int i=0;i<n;i++)
		{
			data[i]=s.data[i];
		}
		data[n]='\0';
	}  
#endif 
	String(const char *a=nullptr)//构造函数
	{
		cout<<"执行了构造函数"<<endl;
		if(a==0)
		{
			data=0;n=0;
			return;
		}
		const char* p=a;
		while(*p)p++;
		n=p-a;
		data=new char[n+1];
		for(int i=0;i<n;i++)//这里复制数组到data,方便以下的重载和size返回
		{
			data[i]=a[i];
		}
		data[n]='\0';
	}
	char& operator[](int i)//重载[]
	{
		if(i<0||i>=n)throw "下标异常!";
		return data[i];
	}
	int size()const//返回字符串大小
	{
		return n;
	}
};
ostream& operator<<(ostream& o,String a)//重载<<
{
	for(int i=0;i<a.size();i++)
	{
		o<<a[i];
	}
	return o;
}
int main()
{
	String str("hello");//构造
	cout<<str;//重载拷贝,因为重载<<传入的String参数为str的副本不是str本身,所以会新开辟一块空间。而如果使用String& a将不会有该操作
	//析构,释放重载<<中的临时变量
	
	String str1=str;//重载拷贝(重载时创建了一个str1新对象,所以也会开辟一块新空间)
	
	str1[1]='E';//通过重载拷贝和<<,达到只修改str1对象的目的
	
	cout<<str1;//重载拷贝
	//析构,释放重载<<中的临时变量
	
	cout<<str;//重载拷贝
	//析构,释放重载<<中的临时变量
	
	//这里先是释放了str1,因为str1先离开了作用域
	//然后再释放str
	return 0;
}

输出:

执行了构造函数
执行了拷贝函数 hello执行了析构函数
执行了拷贝函数 执行了拷贝函数 hEllo执行了析构函数
执行了拷贝函数 hello执行了析构函数
执行了析构函数(str1释放)
执行了析构函数(str释放)

12.类模板: (函数模版拓展)

 这里我们用vector容器举例,来实现类似vector的功能

#include<iostream>
#include<vector>
#include<string>
using namespace std;
template<typename T>
class Vector
{
	T* data;
	int max_index;
	int n;
public :
	Vector(int n=5)//构造函数
	{
		data=new T[n];
		if(data==0)
		{
			max_index=0;
			this->n=0;
			return;
		}
		max_index=n;
		this->n=0;
	}
	void Push_back(T e)//增加元素
	{
		if(n==max_index)//容量已满
		{
			cout<<"空间已增加!\n";
			T* p=new T[2*max_index];
			if(p)
			{
				for(int i=0;i<n;i++)
				{
					p[i]=data[i];
				}
				delete[] data;
				data=p;
				max_index=2*max_index;
			}
			else
			{
				return;
			}
		}
		data[n]=e;
		n++;
	}
	T operator[](int i)//重载[]
	{
		if(i<0||i>n)
		{
			throw "不在规定范围!\n";
		}
		return data[i];
	}
	int size()
	{
		return n;
	}
};
int main()
{
	Vector<string> a;
	a.Push_back("1");
	a.Push_back("hello");
	a.Push_back("2");
	a.Push_back("world");
	a.Push_back("3");
	for(int i=0;i<a.size();i++)
	{
		cout<<a[i]<<endl;
	}
	
	return 0;
	
}

 这里我们创建了一个模版类,用它可以类似实现vector的指定类型数据容器的创建,从而更好地研究某些函数的内部构成和具体使用方式。

NO.48

好了各位小伙伴,以上是大概的C++学习内容,要是像继续深入学习请期待后续更新!!!

有任何问题可以@作者,感谢您的支持❀