C++学习笔记

99 阅读31分钟

C++学习网:www.studycpp.cn/

初级篇

基础语法

变量与常量

变量初始化

C++中有6种基本的变量初始化方法:

int a; // 默认初始化,变量的值不确定
int b = 5; // 拷贝初始化
int c( 6 ); // 直接初始化

// 列表初始化 (C++11引进)
int d { 7 }; // 直接列表初始化
int e = { 8 }; // 拷贝列表初始化
//值初始化将变量初始化为零(或空,如果这更适合给定类型)。发生归零的这种情况,称为零初始化。
int f {}; // 值列表初始化
常量
  • C++支持两种不同类型的常量:
    1. 命名常量(Named constants)是与标识符关联的常量值。这些有时也被称为符号常量(symbolic constants),或者只称为常量(constants)。
    2. 字面值常量(Literal constants)是与标识符无关的常量值。
  • 在C++中定义命名常量有三种方法:
    1. 将变量设置为不可更改,即为常变量(Constant variables)。
      • 定义常变量时必须对其进行初始化,之后不能通过赋值更改该值
      • 通过值传递时不要使用const。
      • 按值返回时不要使用const。
    2. 具有替换文本的类对象宏。
      • 定义常量时,不推荐使用类对象宏。
    3. 枚举常数。
  • as-if规则: 编译器可以随意修改程序,以产生更优化的代码,只要这些修改不影响程序的“可观察行为”。
  • 常量表达式(constant expression)是编译器可以在编译时计算的表达式。要成为常量表达式,表达式中的所有值都必须在编译时已知(并且调用的所有运算符和函数都必须支持编译时求值)。表达式 “3 + 4” 是一个常量表达式。因此,当编译该程序时,编译器可以将其替换为结果值7。
  • 编译时常量是其值为常量表达式的常量。字面值(例如 1、2.3 和“Hello,world!”)是编译时常量的一种类型。常变量可以是也可以不是编译时常量(取决于它们的初始化方式)。
  • 如果常变量的初始值是非常量表达式,则是运行时常量。运行时常量是在运行时之前无法确定其初始化值的常量。
  • 当使用const时,变量是编译时常量或运行时常量,这取决于初始值是否是编译时常量表达式。在某些情况下,很难区分。

例如:

int x { 5 };       // 非 const
const int y { x }; // 运行时常量 (因为使用非const变量初始化)
const int z { 5 }; // 编译时常量
const int w { getValue() }; // 不是很容易判断

幸运的是,我们可以获得编译器的帮助,以确保在需要的地方获得编译时常量。为此,我们在变量的声明中使用constexpr关键字而不是const。constexpr(“常量表达式”的缩写)变量只能是编译时常量。如果constexpr变量的初始化值不是常量表达式,编译器将报错。

#include <iostream>
int six() {
	return 6;
}

int main() {
	constexpr double num{ 9.0 };
	constexpr int i{ 2 + 1 };
	constexpr int j{ i };

	constexpr int f{ six() }; // 编译报错
	return 0;
}
  • 普通函数调用在运行时求值。这意味着函数参数即使是编译时常量,也会被视为运行时常量。
  • 字面值常量:
    • 字面值是直接插入到代码中的值。
    • 所有字面值都有类型。字面值的类型是从字面值的值中推导出来的。例如,作为整数(例如5)的字面值被推断为int类型。
    • 默认情况下,浮点字面值的类型为double。要使它们成为float字面值,应使用F(或f)后缀。
    • “Hello,world” 是一个字符串字面值。这样的字符串通常称为C字符串或C样式的字符串,因为它们是从C语言继承的。所有的C字符串,都有一个隐式的null结尾符。例如"hello",看起来只有五个字符,但实际是6个 ‘h’, ’e’, ’l‘, ’l’, ‘o’, and ‘\0’ (ASCII 码 0)。不像其它大多数字面值常量(它们都是值,而非对对象),c字符串是一个const的对象,程序开始执行前会确保存在,直到程序结束。与C样式字符串字面值不同,std::string和std::string_view字面值会创建临时对象。这些临时对象会立即使用,在创建它们的完整表达式的末尾被销毁。

数据类型

...

头文件保护

使用头文件,很容易导致头文件中的定义被多次包含。当头文件#include另一个头文件(这是常见的)时,可能会发生这种情况。好消息是,我们可以通过一种称为头文件保护(也称为包含保护)的机制来避免上述问题。头文件保护的目标是防止代码文件引入头文件的多个副本

square.h

#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides()
{
    return 4;
}
#endif // !SQUARE_H

wave.h

#ifndef WAVE_H
#define WAVE_H


#include "square.h"

#endif // !WAVE_H

main.cpp

#include "square.h"
#include "wave.h"
#include <iostream>

int main()
{
    std::cout << "测试头文件保护";
    return 0;
}

头文件保护不会阻止头文件在不同文件只出现一次。根据设计,头文件保护不会阻止给定的头文件被包含到不同的代码文件中。这也可能导致意外问题。解决此问题的最佳方法是将函数定义放在其中一个.cpp文件中,以便头文件仅包含向前声明。

square.h

#ifndef SQUARE_H
#define SQUARE_H

int getSquareSides(); //向前声明
int getSquarePerimeter(int sideLength); //向前声明

#endif // !SQUARE_H

square.cpp

#include "square.h"

int getSquareSides()
{
	return 4;
}
int getSquarePerimeter(int sideLength) {
	return sideLength * getSquareSides();
}

main.cpp

#include "square.h"
#include "wave.h"
#include <iostream>

int main()
{
    std::cout << "测试头文件保护";
    return 0;
}

现代编译器使用#pragma预处理器指令支持更简单的替代形式的头文件保护:一些开发公司(如Google)建议使用传统的头文件保护。

#pragma once 
// your code here

运算符

#include <iostream>
using namespace std;

int main() {
	int a = 20;
	int b = 10;
	int c;
	c = a > b ? a : b;
	cout << "c=" << c << endl;
	system("pause");
	return 0;
}

程序流程结构

if

#include <iostream>
using namespace std;

int main() {
	int score = 0;
	cout << "请输入一个数字:" << endl;
	cin >> score;
	if (score >= 6) {
		cout << "合格了" << endl;
	}
	else {
		cout << "不合格" << endl;
	}
	system("pause");
	return 0;
}

for

#include <iostream>
using namespace std;

int main() {
	int num = 0;
	for (int i = 0; i < 10; i++)
	{
		if (i == 5) {
			break;
		}
		cout << i << endl;
	}
	system("pause");
	return 0;
}

goto

#include <iostream>
using namespace std;

int main() {
	//可以无条件跳转到标记的位置
	cout << "1" << endl;
	goto FLAG;
	cout << "2" << endl;
	cout << "3" << endl;
	cout << "4" << endl;
	FLAG:
	cout << "5" << endl;
	system("pause");

	return 0;
}

while

#include <iostream>
using namespace std;

int main() {
	int num = 0;
	while (num < 10)
	{
		cout << num << endl;
		num++;
	}
	system("pause");

	return 0;
}

数组

#include <iostream>
using namespace std;

int main() {
	//一维数组
	int arr1[5];
	int arr2[10] = { 1,2,3 };
	int arr3[] = { 4,5,6,7 };
	
	for (int i = 0; i < sizeof(arr2) / sizeof(arr2[0]); i++)
	{
		cout << arr2[i] << endl;
	}
	cout << "整个数组所占内存空间为:" << sizeof(arr2) << endl;
	cout << "每个元素所占内存空间为:" << sizeof(arr2[0]) << endl;
	cout << "数组的元素个数为:" << sizeof(arr2) / sizeof(arr2[0]) << endl;
	//通过数组名获取数组首地址,默认为十六进制,强转为int就是十进制
	cout << "数组首地址为:" << (int)arr2 << endl;
	cout << "数组第2个元素的地址为:" << (int) & arr2[1] << endl;
	system("pause");

	return 0;
}
#include <iostream>
using namespace std;

int main() {
	//二维数组定义的四种方式
	int arr[2][3];
	int arr2[3][2] = { {1,2},{3,4} };
	int arr3[2][2] = { 5,6,7,8 };
	int arr4[][3] = { 9,10,11,12,13,14 };
	//查看二维数组所占内存空间
	cout << "二维数组大小:" << sizeof(arr) << endl;
	cout << "二维数组一行大小:" << sizeof(arr[0]) << endl;
	cout << "二维数组元素大小:" << sizeof(arr[0][0]) << endl;
	cout << "二维数组行数:" << sizeof(arr) / sizeof(arr[0]) << endl;
	//获取二维数组首地址
	cout << (int)arr << endl;
	//二维数组第一行首地址
	cout << (int)arr[0] << endl;
	//第二行首地址
	cout << (int)arr[1] << endl;
	//二维数组第一个元素地址
	cout << (int)&arr[0][0] << endl;
	system("pause");
	return 0;
}

函数

#include "sayHi.h"

void sayHi() {
	cout << "嗨!!!" << endl;
}
#include <iostream>
#include "sayHi.h"
using namespace std;

int add(int sum1, int sum2) {
	return sum1 + sum2;
}
void sayHello(string name) {
	cout << "你好," << name << endl;
}
void sayBy(int year);
int main() {
	/*
	* 1.函数语法:
	*	返回值类型 函数名 (参数列表){
	*		函数体语句
	*		return 表达式
	*	}
	* 2.如果函数不需要返回值,声明的时候可以写 void
	* 3. 所谓值传递,就是函数调用时实参将数值传入给形参,如果形参在调用函数期间发生改变,
		 并不会影响实参
	* 4. 函数的声明与定义:函数的声明可以多次,但是函数的定义只能有一次。
	* 5. 函数的分文件编写:
	*		1)创建后缀名为.h的头文件
	*		2)创建后缀名为.cpp的源文件
	*		3)在头文件中写函数的声明;在源文件中写函数的定义
	*		4)在使用函数的源文件中引入头文件
	*/
	cout << add(1, 2) << endl;
	sayHello("张三");
	sayBy(1998);
	sayHi();
	system("pause");
	return 0;
}
void sayBy(int year){
	cout << year << ",拜拜" << endl;
}


自定义命名空间和作用域解析操作符

  • C++允许我们通过namespace关键字定义自己的命名空间。在程序中创建的命名空间称为用户定义命名空间。
  • 建议以大写字母开始命名空间名称。
  • 使用域解析操作符( :: )访问命名空间。
  • 域解析操作符也可以在标识符之前使用,而不提供命名空间名称(例如 ::doSomething)。在这种情况下,在全局命名空间中查找标识符。
  • 如果使用命名空间内的标识符,并且没有提供域解析,编译器将首先尝试在同一命名空间中查找匹配的声明。如果没有找到匹配的标识符,编译器将依次检查外围每个层级的命名空间,以查看是否找到匹配,直到检查全局命名空间。
  • 对于命名空间内的标识符,前向声明也需要在同一命名空间内。
  • 在多个位置(跨多个文件或同一文件中的多个位置)声明命名空间块是合法的。命名空间中的所有声明都被视为命名空间的一部分。
  • 命名空间可以嵌套在其他命名空间中。
  • 由于在嵌套命名空间中键入变量或函数的限定名可能会很痛苦,C++允许您创建命名空间别名。

add.h

#pragma once
namespace BasicMath {
	int add(int a, int b);
}

subtract.h

#pragma once
namespace BasicMath {
	int sub(int x, int y);
}

add.cpp

#include "add.h"

namespace BasicMath {
	int add(int a, int b) {
		return a + b;
	}
}

subtract.cpp

#include "subtract.h"

namespace BasicMath {
	int sub(int x, int y) {
		return x - y;
	}
}

Main.cpp

#include <iostream>
#include "add.h"
#include "subtract.h"

void print() {
	std::cout << "全局的输出方法" << std::endl;
}
namespace Dog {
	void print() {
		std::cout << "小狗画梅花" << std::endl;
	}
	void action() {
		std::cout << "狗在叫" << std::endl;
		print();
		::print(); // 调用 全局命名空间中的 print()
	}
}
namespace Cat {
	void action() {
		std::cout << "猫在跑步" << std::endl;
	}
}
namespace Child {
	namespace Eat {
		void eatApple() {
			std::cout << "孩子吃苹果" << std::endl;
		}
	}
}
int main() {
	
	/*Dog::action();
	Cat::action();*/
	/*print();
	::print();
	Dog::print();*/
	//Dog::action();
	int res = BasicMath::add(1, 2);
	std::cout << res << "\n";
	std::cout << BasicMath::sub(2, 1) << std::endl;

	//Child::Eat::eatApple();
	namespace AboutChild = Child::Eat;
	AboutChild::eatApple();
	system("pause");
	return 0;
}

类型转换和static_cast

#include <iostream>
void print(int x) {
	std::cout << x << std::endl;
}

int main() {
	//print(5.5); // warning C4244: “参数”: 从“double”转换到“int”,可能丢失数据
	//double d{ 5 };
	//int x{ 5.5 };//error
	//显式类型转换 static_cast<新类型>(表达式)
	print(static_cast<int>(5.5));
	//将char转为int
	char ch{ 97 };
	std::cout << ch << '\n';  //a
	std::cout << static_cast<int>(ch) << '\n'; //97
	//将无符号数字转换为有符号数字(如果要转换的值不适合新类型的范围,static_cast操作符将产生未定义的行为。)
	unsigned int u{ 5 };
	int s{ static_cast<int>(u) };
	std::cout << s << '\n';
	return 0;
}

指针

#include <iostream>
using namespace std;

int main() {
	/*
	* 1.可以通过指针间接访问内存
	* 2.内存编号是从0开始记录的,一般用十六进制表示
	* 3.可以利用指针变量保存地址
	* 4.指针变量定义语法:数据类型 * 变量名;
	*/
	int a = 10;
	int* p;
	p = &a;
	cout << "a的地址为:" << &a << endl;
	cout << "指针p:" << p << endl;
	//通过解引用的方式找到指针指向的内存
	*p = 50;
	cout << "p指向的内存中的数据:" << *p << endl;
	cout << "a:" << a << endl;
	//指针所占用的内存空间(64位操作系统占用8个字节,32位操作系统占用4字节)
	cout << "p所占用的内存空间:" << sizeof(p) << endl;
	cout << "double类型指针所占用的内存空间:" << sizeof(char *) << endl;
	cout << "string类型指针所占用的内存空间:" << sizeof(string *) << endl;
	system("pause");
	return 0;
}
#include <iostream>
using namespace std;
void swap1(int c, int d);
void swap2(int* p1, int* p2);
void arrSortAsc(int* p,int len);
int main() {
	/*
	* 1.空指针:指针变量指向内存中编号为0的空间
	*			用途:初始化指针变量
	*	注意:空指针指向的内存是不可以访问的。
	* 2.野指针:指针变量指向非法的内存空间。
	* 3.const修饰指针:
	*		1)const修饰指针:常量指针,指针的指向可以改,但是指针指向的值不可以改
	*		2)const修饰常量:指针常量,指针的指向不可以改,但是指针指向的值可以改
	*		3)const既修饰指针,又修饰常量。指针的指向和指针指向的值都不可以改
	* 4.利用指针访问数组中的元素
	* 5.利用指针作为函数参数,可以修改实参的值!
	*/
	//int* p = NULL;
	//内存编号0~255为系统占用内存,不允许用户访问
	//cout << *p << endl;
	
	//野指针
	/*int* p = (int *)0x1100;
	cout << *p << endl;*/

	//常量指针
	int a = 10;
	int b = 30;
	const int* p = &a;
	//*p = 20;//错误
	p = &b;
	//指针常量
	int* const p2 = &a;
	//p2 = &b; //错误
	*p2 = 100;
	//const修饰指针和常量
	const int* const p3 = &a;
	/*p3 = &b;
	*p3 = 200;*/

	//利用指针来访问数组中的元素
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	cout << "数组中第一个元素:" << arr[0] << endl;
	int* p4 = arr;
	cout << "利用指针访问数组第一个元素:" << *p4 << endl;
	cout << "数组中每个元素的大小为:" << sizeof(arr[0]) << endl;
	//指针向后偏移四个字节
	//p4++;
	//cout << "偏移后的结果:" << *p4 << endl;
	/*cout << "利用指针遍历数组" << endl;
	for (int i = 0; i < 10; i++)
	{
		cout << *p4 << endl;
		p4++;
	}*/
	int c = 13;
	int d = 16;
	swap1(c, d);
	cout << "c=" << c << "d=" << d << endl;
	swap2(&c, &d);
	cout << "c=" << c << "d=" << d << endl;
	//封装一个函数,利用冒泡排序,实现对整型数组的升序排序
	int arr2[5] = { 4,2,5,1,3 };
	int* p5 = arr2;
	int len = sizeof(arr2) / sizeof(arr2[0]);
	arrSortAsc(p5,len);
	for (int i = 0; i < 5; i++)
	{
		cout << arr2[i] << endl;
	}
	system("pause");
	return 0;
}
//值传递
void swap1(int c, int d) {
	int temp = c;
	c = d;
	d = temp;
}
//地址传递
void swap2(int* p1, int* p2) {
	int temp = *p1;
	*p1 = *p2;
	*p2 = temp;
}
void arrSortAsc(int* p,int len) {
	for (int i = 0; i < len-1; i++)
	{
		for (int j = 0; j < len-1-i ; j++)
		{
			if (p[j] > p[j + 1]) {
				int temp = p[j + 1];
				p[j + 1] = p[j];
				p[j] = temp;
			}
		}
	}
}

结构体

#include <iostream>

using namespace std;
struct User
{
	//成员列表
	string name;
	int age;
	int score;
}s3; //顺便创建结构体变量

void printUser(User user) {
	cout << "(值传递)用户的姓名:" << user.name << endl;
}
void printUser2(const User* user) {
	//user->age = 13; //操作失败,因为加了const
	cout << "(地址传递)用户的姓名:" << user->name << endl;
}
int main() {
	/*
	* 1.结构体属于用户自定义的数据类型,允许用户存储不同的数据类型
	* 2.语法:struct 结构体名 {结构体成员列表};
	* 3.通过结构体创建变量的方式有三种:注意:struct关键字可以省略不写
	*		1)struct 结构体名 变量名;
	*		2)struct 结构体名 变量名 = {成员1值,成员2值};
	*		3)定义结构体时顺便创建变量(不建议)
	* 4.结构体数组
	*		1)作用:将自定义的结构体放入到数组中方便维护
	*		2)语法:struct 结构体名 数组名[元素个数] = {{},{},...{}};
	* 5.结构体指针
	*		1)通过指针访问结构体中的成员
	*		2)利用操作符 -> 可以通过结构体指针访问结构体属性
	* 6.结构体嵌套:结构体中的一个成员可以是另一个结构体
	* 7.结构体做函数参数
	*		1)作用:将结构体作为参数向函数中传递,传递方式有两种
	*				1)值传递
	*				2)地址传递
	* 8.结构体中const使用场景:用const来防止误操作
	*/
	struct Student
	{
		//成员列表
		string name;
		int age;
		int score;
	}s3; //顺便创建结构体变量
	Student s1;
	s1.name = "汤姆";
	s1.age = 89;
	s1.score = 100;
	cout << "s1的名字:" <<  s1.name << endl;
	
	Student s2 = { "tom",23,200};
	cout << "s2的分数:" << s2.score << endl;

	s3.name = "杰瑞";
	s3.age = 8;
	s3.score = 300;
	cout << "s3的分数:" << s3.score << endl;

	//结构体数组
	Student arr[3] = {
		{ "张三",13,400},
		{ "李四",42,500},
		{ "王五",51,600}
	};
	//给结构体数组中的元素赋值
	arr[0].name = "张三2";
	arr[2].age = 3;
	//遍历结构体数组
	cout << "==================" << endl;
	for (int i = 0; i < 3; i++)
	{
		cout << "姓名:" << arr[i].name << endl;
		cout << "年龄:" << arr[i].age << endl;
		cout << "分数:" << arr[i].score << endl;
	}
	//结构体指针
	cout << "==================" << endl;
	Student* p = &s2;
	cout << p->name << endl;
	//结构体嵌套
	cout << "==================" << endl;
	struct Teacher
	{
		int id;
		string name;
		int age;
		Student stu;
	};
	Student s4 = { "学生甲",23,110 };
	Teacher t1 = { 001,"老师甲",34,s4 };
	cout << "老师的姓名:" << t1.name << endl;
	cout << "老师的学生的姓名:" << t1.stu.name << endl;
	//结构体做函数参数
	cout << "==================" << endl;
	User u1 = { "tomla",23,100 };
	//值传递
	//printUser(u1);
	//地址传递
	printUser2(&u1);
	system("pause");
	return 0;
}

引用

new关键字

#include <iostream>
using namespace std;
int* func() {
	//在堆区创建一个数据,new返回的是该数据类型的指针
	int* p = new int(10);
	return p;
}
void test01() {
	int* p = func();
	cout << *p << endl;
	delete p;
	cout << *p << endl;
}
void test02() {
	//在堆区开辟一个数组,数组有五个元素
	int* arr = new int[5];
	for (int i = 0; i < 5; i++) {
		arr[i] = i + 100;
	}
	for (int i = 0; i < 5; i++) {
		cout << arr[i] << endl;
	}
	//释放堆区中的数组
	delete[] arr;
}
int main() {
	/*
	* 1.C++中利用new操作符在堆区开辟数据:
	*		语法:new 数据类型
	* 2.堆区开辟的数据,由程序员手动开辟,手动释放利用操作符:delete 指针
	* 3.利用new创建的数据,会返回该数据对应的类型的指针
	* 4.释放堆区中的数组时要加[],例如:delete[] 指针;
	*/
	//test01();
	test02();
	system("pause");
	return 0;
}

引用

#include <iostream>
using namespace std;
//两个数字交换
//地址传递
void swapByAddr(int* a, int* b) {
	int temp;
	temp = *b;
	*b = *a;
	*a = temp;
}
//引用传递
void swapByRefer(int &a,int &b) {
	int temp = a;
	a = b;
	b = temp;
}
int& test01() {
	int a = 10;
	return a;
}
int& test02() {
	static int a = 10; //静态变量,存放在全局区,全局区上的数据由系统释放
	return a;
}
int main() {
	/*
	* 1.引用的作用:给变量起别名
	* 2.语法:数据类型 &别名 = 原名
	* 3.注意事项:1)引用必须初始化;2)引用在初始化后,不可以改变
	* 4.引用做函数参数:
	*		1)函数传参时,可以利用引用让形参修饰实参
	*		2)可以简化指针修改实参
	* 5.引用做函数返回值
	*		1)注意:不要返回局部变量引用
	*		2)用法:函数调用作为左值(如果函数的返回值是引用,这个函数调用可以作为左值)
	*/
	//int a = 10;
	////创建引用
	//int& b = a;
	//cout << a << endl;
	//cout << b << endl;
	//b = 20;
	//cout << a << endl;
	//cout << b << endl;
	/*int a = 50;
	int b = 80;
	swapByAddr(&a, &b);
	cout << "a=" << a << endl;
	cout << "b=" << b << endl;
	swapByRefer(a, b);
	cout << "a=" << a << endl;
	cout << "b=" << b << endl;*/
	int& ref = test01();
	cout << ref << endl;
	cout << ref << endl;

	int& ref2 = test02();
	cout << ref2 << endl;
	test02() = 1000;
	cout << ref2 << endl;
	system("pause");
	return 0;
}

引用的本质

#include <iostream>
using namespace std;
void func(int& a) {
	//自动转换为 *a = 100;
	a = 100;
}
int main() {
	/*
	* 1.引用的本质:在C++内部实现是一个指针常量
	* 2.推荐使用引用,因为引用的本质是指针常量,所有指针操作,编译器都帮我们做了 
	*/
	int a = 0;
	//自动转换为 int* const ref = &a; 这也解释了为什么引用的指向不可更改
	int& ref = a;
	//自动转换为 *ref = 20;
	ref = 20;
	cout << a << endl;
	func(a);
	cout << a << endl;
	system("pause");
	return 0;
}

类和对象

基础用法

#include <iostream>
using namespace std;

class C1
{
  //将成员属性设置为私有,自己控制读写权限
  int m;
  public:
    void setM(int m){
      this->m = m;
    }
    int getM(){
      return m;
    }
};
struct C2
{
  int m;
};
int main()
{
  //struct默认权限是 public
  //class默认权限是 private
  C1 c1;
  // c1.m
  C2 c2;
  c2.m = 10;
  c1.setM(20);
  cout << c1.getM() << endl;

  system("pause");
  return 0;
}

析构函数

#include <iostream>
using namespace std;

class Person
{
public:
  Person()
  {
    cout << "aaaa" << endl;
  }
  ~Person(){
    cout << "bbbb" << endl;
  }
};
void test01(){
  Person p;
}
int main()
{
  // 析构函数:主要作用在于对象销毁前系统自动调用,执行一些清理工作
  /**
   * 1.析构函数的声明:~类名(){}
   * 2.析构函数不可以有参数,因此不可以发生重载
   * 3.构造函数和析构函数都是必须的,如果我们不提供,则编译器会提供空的构造和析构实现
   */
  test01();
  system("pause");
  return 0;
}

构造函数的分类

  1. 有参和无参构造函数
  2. 按类型:普通构造和拷贝构造
class Person
{
public:
  Person()
  {
    cout << "无参构造函数Person()" << endl;
  }
  Person(int a)
  {
    cout << "有参构造函数Person(int a)" << endl;
    age = a;
  }
  ~Person()
  {
    cout << "~Person()" << endl;
  }
  //拷贝构造函数
  Person(const Person &p){
    //将传入的p身上的属性拷贝到当前对象的age身上
    age = p.age;
  }
  int age;
};

构造函数的调用方式

#include <iostream>
using namespace std;

class Person
{
public:
  Person()
  {
    cout << "无参构造函数Person()" << endl;
  }
  Person(int a)
  {
    cout << "有参构造函数Person(int a)" << endl;
    age = a;
  }
  ~Person()
  {
    cout << "~Person()" << endl;
  }
  //拷贝构造函数
  Person(const Person &p){
    //将传入的p身上的属性拷贝到当前对象的age身上
    age = p.age;
  }
  int age;
};
void test01(){
  //括号法(注意:调用无参构造函数时,不要加括号,否则编译器会认为这是一个函数的声明,例如Person p();)
  Person p; //调用无参构造函数
  Person p1(10); //调用有参构造函数
  Person p3(p1);
  cout << "p3的年龄:" << p3.age << endl;

  //显示法 (注意:不要利用拷贝构造函数来初始化匿名对象,例如Person(p3),编译器会认为这是一个对象的声明,等同于Person p3;)
  Person p1;
  Person p2 = Person(10); //有参构造   Person(10)单拿出来为匿名对象,特点:当前行执行结束后,系统会立即回收掉匿名对象
  Person p3 = Person(p2); //拷贝构造

  //隐式转换法
  Person p4 = 10; //相当于 Person p4 = Person(10);
  Person p5 = p4; //拷贝构造
}
int main()
{
  test01();
  system("pause");
  return 0;
}

拷贝构造函数调用时机

#include <iostream>
using namespace std;

class Person
{
public:
  Person()
  {
    cout << "Person()" << endl;
  }
  Person(int age)
  {
    cout << "Person(int age)" << endl;
    m_age = age;
  }
  ~Person()
  {
    cout << "~Person()" << endl;
  }
  Person(const Person &p)
  {
    cout << "Person(const Person &p)" << endl;
    m_age = p.m_age;
  }
  int m_age;
};

void test01()
{
  // 1. 使用一个已经创建完毕的对象来初始化一个新对象
  Person p1(20);
  Person p2(p1);
  cout << "p2.age = " << p2.m_age << endl;
}
void doWork(Person p){
  
}
void test02(){
  // 2. 值传递的方式给函数参数传值
  Person p;
  doWork(p);
}
Person doWork02(){
  Person p1;
  cout << &p1 << endl;
  return p1;
}
void test03(){
  // 3. 以值方式返回局部对象(实测并没有调用拷贝构造函数)
  Person p = doWork02();
  cout << &p << endl;
}

int main()
{
  /*
    拷贝构造函数调用时机的三种情况:
      1. 使用一个已经创建完毕的对象来初始化一个新对象
      2. 值传递的方式给函数参数传值
      3. 以值方式返回局部对象
  */
  // test01();
  // test02();
  test03();
  system("pause");
  return 0;
}

构造函数调用规则

  • 默认情况下,编译器至少给一个类添加三个函数
    1. 默认构造函数(无参,函数体为空)
    2. 默认析构函数(无参,函数体为空)
    3. 默认拷贝构造函数,对属性进行值拷贝
  • 构造函数调用规则如下:
    1. 如果用户定义有参构造函数,c++不再提供默认无参构造,但是会提供默认拷贝构造函数
    2. 如果用户定义拷贝构造函数,c++不再提供其他的普通构造函数了,但是会提供默认析构函数

浅拷贝与深拷贝

  • 浅拷贝:简单的赋值操作
  • 深拷贝:在堆区重新申请空间,进行拷贝操作
  • 浅拷贝带来的问题:对象p1在堆区存放的数据,使用默认的拷贝构造函数进行的是浅拷贝,即把堆区的内存地址赋值给了p2,因此在析构函数中释放堆区内存时,p1和p2释放的是同一个地址的内存,p2释放之后,p1就无法释放。解决方案是使用深拷贝,自定义一个拷贝构造函数,p2在堆区重新开辟空间,将p1的内容拷贝到新的堆区内存中,这样p1和p2指向的堆内存不同,但是内容相同。
#include <iostream>
using namespace std;
class Person
{
public:
  Person()
  {
    cout << "Person无参构造函数" << endl;
  }
  Person(int age, int height)
  {
    m_Age = age;
    // 将数据创建在堆区
    m_Height = new int(height);
    cout << "Person有参构造函数" << endl;
  }
  ~Person()
  {
     // 析构中将堆区开辟的数据释放掉
     //浅拷贝带来的问题:堆区的内存重复释放
     //解决方案:使用深拷贝,p1和p2各指向一个堆区数据,这两个堆区存放的数据是相同的,但指针指向的内存是不一样的
     if(m_Height != NULL){
      delete m_Height;
      m_Height = NULL;
     }
    cout << "Person析构函数" << endl;
  }
  //自己实现拷贝构造函数
  Person(const Person &p){
    cout << "Person拷贝构造函数被调用了" << endl;
    m_Age = p.m_Age;
    // m_Height = p.m_Height; //这是编译器的默认实现
    m_Height = new int(*p.m_Height);
  }
  int m_Age;
  int *m_Height;
};
void test01()
{
  Person p1(10, 170);
  cout << "p1的年龄为:" << p1.m_Age << "身高为:" << *p1.m_Height << endl;
  Person p2(p1);
  cout << "p2的年龄为:" << p2.m_Age << "身高为:" << *p2.m_Height << endl;
}
int main()
{
  test01();
  system("pause");
  return 0;
}

初始化列表

#include <iostream>
using namespace std;

class Person
{
public:
  Person(int a, int b, int c) : m_A(a), m_B(b), m_C(c) {}
  int m_A;
  int m_B;
  int m_C;
};
void test(){
  Person p(10, 20, 30);
  cout << p.m_A << endl;
}
int main()
{
  /*
    C++提供了初始化列表语法,用来初始化属性
      语法:构造函数():属性1(值1),属性2(值2)...{};
  */
  test();
  system("pause");
  return 0;
}

对象成员

#include <iostream>
using namespace std;

class Phone
{
public:
  Phone(string pName)
  {
    cout << "Phone(string pName)" << endl;
    name = pName;
  }
  ~Phone(){
    cout << "~Phone()" << endl;
  }
  string name;
};

class Person
{
public:
  // 相当于   Phone phone = pName; //隐式转化法
  Person(string name, string pName) : name(name), phone(pName)
  {
    cout << "Person(string name, string pName)" << endl;
  }
  ~Person(){
    cout << "~Person()" << endl;
  }
  string name;
  Phone phone;
};
void test()
{
  Person p("tom", "apple");
  cout << p.name << ":" << p.phone.name << endl;
}
int main()
{
  /*
    类对象作为类成员
      C++类中的成员可以是另一个类的对象,我们称该成员为对象成员
  */
  test();
  system("pause");
  return 0;
}

静态成员

#include <iostream>
using namespace std;

class Person
{
public:
    static int m_A;
    static void func() {
        cout << "static void func()..." << endl;
        cout << "m_A=" << m_A << endl;
        //cout << "m_C=" << m_C << endl;
        cout << "m_B=" << m_B << endl;
    }
private:
    int m_C;
    static int m_B;
    static void func2() {
        cout << "static void func2()" << endl;
    }
};

int Person::m_A = 10;
int Person::m_B = 20;
void test01() {
    Person p;
    cout << "p.m_A = " << p.m_A << endl;

    Person p2;
    p2.m_A = 20;

    cout << "p2.m_A = " << p2.m_A << endl;
}
void test02() {
    //通过对象进行访问
    Person p;
    cout << p.m_A << endl;
    //通过类名进行访问
    cout << Person::m_A << endl;

    //cout << Person::m_B << endl; //无法访问
}
void test03() {
    //通过对象调用
    Person p;
    p.func();
    //通过类名调用
    Person::func();
    //Person::func2(); //无法访问
}
int main()
{

    /*
      静态成员就是在成员变量和成员函数前加关键字static,称为静态成员
        1. 静态成员变量: 所有对象都共享同一份数据; 编译阶段就分配内存; 类内声明,类外初始化操作。
            访问方式:
              1. 通过对象进行访问
              2. 通过类名进行访问
            静态成员变量也是有访问权限的
            静态成员变量需要初始化后才能被访问到!!!
        2. 静态成员函数:静态成员函数只能访问静态成员变量;也是有两种访问方式
    */

    //test01();
    // test02();
    test03();
    system("pause");
    return 0;
}

成员变量和成员函数是分开存储的

#include <iostream>
using namespace std;

class Person
{
  int m_A; //非静态成员变量,是属于类的对象上的
  static int m_B; //静态成员变量,不属于类的对象上
  //非静态成员函数,不属于类的对象上
  void func(){
    cout << "Person::func()" << endl;
  }
  //静态成员函数,不属于类的对象上
  static void func2(){
    cout << "Person::func2()" << endl;
  }
};
int Person::m_B = 10;
void test01()
{
  Person p;
  //空对象占用的内存空间为:1字节
  //C++编译器会给每个空对象分配一个字节的空间,是为了区分对象占内存的位置
  //每个空对象应该有一个独一无二的内存地址
  cout << "sizeof(p):" << sizeof(p) << endl;
}
void test02(){
  Person p;
  // 占用的内存空间为:4字节
  cout << "sizeof(p):" << sizeof(p) << endl;
}
int main()
{
  /*
    成员变量和成员函数是分开存储的
    结论:
      1. 只有非静态成员变量属于类的对象上
      2. 空对象占用内存空间:1字节
  */
  // test01();
  test02();
  system("pause");
  return 0;
}

this指针概念

#include <iostream>
using namespace std;

class Person
{
public:
  Person(int age)
  {
    this -> age = age;
  }
  //注意:如果PersonAddAge函数的返回值是Person,以值的方式返回,就会拷贝一份新的对象(参考拷贝构造函数)
  Person& PersonAddAge(Person &p){
    this->age += p.age;
    return *this;
  }
  int age;
};
void test01()
{
  Person p1(12);
  cout << "p1的年龄为:" << p1.age << endl;
}
void test02(){
  Person p1(20);
  Person p2(20);
  p2.PersonAddAge(p1).PersonAddAge(p1);
  cout << "p2的年龄为:" << p2.age << endl;
}
int main()
{
  /*
    this指针概念:
      1. 每一个非静态成员函数只会有一份函数实例,也就是说多个同类型的对象会共用同一个函数实例,那么如何区分是哪个对象在调用呢?
      2. C++通过提供特殊的对象指针this来区分,this指针指向被调用的成员函数所属的对象
      3. this指针是隐含的,编译器自动给每一个非静态成员函数添加this指针作为参数
      4. this指针的用途:
        1)当形参和成员变量同名时,可用this指针来区分,例如 this->age = age;
        2)在类的非静态成员函数中返回对象本身,可使用 return *this;
  */
  // test01();
  test02();
  system("pause");
  return 0;
}

空指针调用成员函数

#include <iostream>
using namespace std;

class Person
{
  public:
  void showClass(){
    cout << "this is Person Class" << endl;
  }
  void showPersonAge(){
    //age 相当于 this->age,报错原因是 this指针为空
    if(this == NULL){
      return;
    }
    cout << "age:" << age << endl;
  }
  int age;
};
void test01(){
  Person *p = NULL;
  p->showPersonAge();
  p->showClass();
}
int main()
{
  /*
    C++中空指针也是可以调用成员函数的,但是也要注意有没有用到this指针
    如果用到this指针,需要加判断以保证代码的健壮性
  */
  system("pause");
  return 0;
}

常函数和常对象

#include <iostream>
using namespace std;

class Person
{
public:
  int m_A;
  mutable int m_B;
  // void showPerson() const 中的const等同于将this指针修改为:const Person* const this
  void showPerson() const
  {
    // this指针的本质是指针常量,在这里是Person* const this,指针的指向是不可修改的
    // m_A = 100;//错误 常函数不能修改成员变量的值
    m_B = 100; // 正确 常函数中可以修改mutable修饰的成员变量的值
  }
  void func()
  {
    m_A = 100;
  }
};
void test01()
{
  Person p;
  p.showPerson();
}
void test02()
{
  const Person p; // 在对象前加const,对象称为常对象
  // p.m_A = 100; //不可以
  p.m_B = 100;
  p.showPerson();
  // p.func(); //不可以
}
int main()
{
  /*
    常函数:
      1. 成员函数后加const我们称这个函数为常函数
      2. 常函数不能修改成员变量的值
      3. 成员属性声明时加关键字mutable后,在常函数中依然可以修改
    常对象:
      1. 声明对象前加const称该对象为常对象
      2. 常对象只能调用常函数
  */
  test01();
  test02();
  system("pause");
  return 0;
}

友元--全局函数做友元

#include <iostream>
using namespace std;
class Building
{
  // goodGay()全局函数做友元
  friend void goodGay(Building &building);

public:
  string m_SittingRoom;

public:
  Building()
  {
    m_SittingRoom = "客厅";
    m_BedRoom = "卧室";
  }

private:
  string m_BedRoom;
};
// 全局函数
void goodGay(Building &building)
{
  cout << "好基友全局函数正在访问:" << building.m_SittingRoom << endl;
  cout << "好基友全局函数正在访问:" << building.m_BedRoom << endl;
}
void test01()
{
  Building building;
  goodGay(building);
}
int main()
{

  /*
    友元的目的:让一个函数或者类可以访问另一个类的私有成员
    友元的关键字为 friend
    友元的三种实现:
      1. 全局函数做友元
      2. 类做友元
      3. 成员函数做友元
  */
  test01();
  system("pause");
  return 0;
}

友元--类做友元

#include <iostream>
using namespace std;
class Building
    //Good类是朋友,可以访问本类中私有成员
{
    friend class GoodGay;
public:
    string m_sittingRoom;
public:
    Building();

private:
    string m_bedRoom;
};
//类外写成员函数
Building::Building() {
    m_sittingRoom = "客厅";
    m_bedRoom = "卧室";
}
class GoodGay
{
public:
    GoodGay();
    void visit();
    Building* building;
};
//GoodGay的构造函数
GoodGay::GoodGay() {
    //创建建筑物
    building = new Building;
}
//类外写成员函数
void GoodGay::visit() {
    cout << "GoodGay类中的visit函数正在访问:" << building->m_sittingRoom << endl;
    cout << "GoodGay类中的visit函数正在访问:" << building->m_bedRoom << endl;
}
void test01() {
    GoodGay g1;
    g1.visit();
}
int main()
{
    /*
      类做友元
    */
    test01();
    system("pause");
    return 0;
}

友元--成员函数做友元

#include <iostream>
using namespace std;

class Building;
class GoodGay
{
public:
	GoodGay();
	Building* building;
	// 让visit可以访问Building中的私有属性
	void visit();
	// 让visit2不可以访问Building中的私有属性
	void visit2();
};

class Building
{
	//告诉编译器,GoodGay下的visit函数作为本类友元
	friend void GoodGay::visit();
public:
	string m_SittingRoom;
	Building();
private:
	string m_bedRoom;
};
//类外实现成员函数
Building::Building() {
	m_SittingRoom = "客厅";
	m_bedRoom = "卧室";
}
GoodGay::GoodGay() {
	building = new Building;
}
void GoodGay::visit() {
	cout << "visit函数正在访问" << building->m_SittingRoom << endl;
	cout << "visit函数正在访问" << building->m_bedRoom << endl;
}
void GoodGay::visit2() {
	cout << "visit2函数正在访问" << building->m_SittingRoom << endl;
	//cout << "visit2函数正在访问" << building->m_bedRoom << endl; //visit2不是Building类的友元
}
void test01() {
	GoodGay gg;
	gg.visit();
	gg.visit2();
}
int main()
{
	test01();
	system("pause");
	return 0;
}

加号运算符重载

#include <iostream>
using namespace std;

// 需求:通过成员函数重载加号运算符;通过全局函数重载加号运算符
class Person
{
public:
  // Person operator+(Person &p){
  //   Person pTemp;
  //   pTemp.m_A = this->m_A + p.m_A;
  //   pTemp.m_B = this->m_B + p.m_B;
  //   return pTemp;
  // }
  int m_A;
  int m_B;
};
//全局函数
Person operator+(Person &p1, Person &p2)
{
  Person pTemp;
  pTemp.m_A = p1.m_A + p2.m_A;
  pTemp.m_B = p1.m_B + p2.m_B;
  return pTemp;
}
Person operator+(Person& p1,int num){
  p1.m_A += num;
  p1.m_B += num;
  return p1;
}
void test01()
{
  Person p1;
  p1.m_A = 10;
  p1.m_B = 10;
  Person p2;
  p2.m_A = 10;
  p2.m_B = 10;
  Person p3;
  p3 = p1 + p2;
  //本质调用
  // p3 = p1.operator+(p2);
  // p3 = operator+(p1,p2);
  cout << "p3.m_A = " << p3.m_A << endl;
  cout << "p3.m_B = " << p3.m_B << endl;
  Person p4 = p1 +5;
  cout << "p4.m_A = " << p4.m_A << endl;
  cout << "p4.m_B = " << p4.m_B << endl;
}
int main()
{
  /**
   * 运算符重载:
   *  1.概念:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。
   * 加号运算符重载
   *  作用:实现两个自定义数据类型相加的运算
   */
  test01();
  system("pause");
  return 0;
}

左移运算符重载

#include <iostream>
using namespace std;

class Person
{
	friend std::ostream& operator<<(std::ostream& out, Person& p);
private:
	// 通常不会使用成员函数重载左移运算符
	int m_A;
	int m_B;
public:
	Person(int a, int b) {
		m_A = a;
		m_B = b;
	}
};
// 全局函数重载左移运算符
std::ostream& operator<<(std::ostream& out ,Person& p)
{
	out << "m_A = " << p.m_A << endl;
	out << "m_B = " << p.m_B << std::endl;
	return out;
}
void test01()
{
	Person p(10,20);
	std::cout << p << "hello" << std::endl;
}
int main()
{
	/**
	 * 左移运算符重载
	 *  作用:可以输出自定义数据类型
	 */
	test01();
	system("pause");
	return 0;
}

递增运算符重载

#include <iostream>
using namespace std;

class MyInteger
{
    friend ostream& operator<<(ostream& cout, const MyInteger& myInt);
public:
    MyInteger()
    {
        m_Num = 0;
    }
    //重载前置++运算符
    MyInteger& operator++() {
        m_Num++;
        return *this;
    }
    //重载后置++运算符,int形参作为占位参数,为了满足函数重载的条件,参数列表不同
        MyInteger operator++(int) {
        //先记录当前值,再递增,然后将记录的值返回
        MyInteger temp = *this;
        //cout << &temp << endl;
        m_Num++;
        return temp;
    }
private:
    int m_Num;
};
ostream& operator<<(ostream& cout, const MyInteger& myInt)
{
    cout << myInt.m_Num;
    return cout;
}

void test01() {
    MyInteger i1;
    cout << ++(++i1) << endl;
    cout << i1 << endl;
}
void test02() {
    MyInteger myint;
    cout << myint++ << endl;
    //cout << &myint << endl;
    cout << myint << endl;
}
int main()
{
    /**
     * 递增运算符重载
     *  作用:通过重载递增运算符,实现自己的整型数据
     */
    //test01();
    test02();
    system("pause");
    return 0;
}

赋值运算符重载

关系运算符重载

函数调用运算符重载

文件操作

中级篇

高级篇