文章参考:黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难



一、内存分区

C++程序在执行时,将内存大方向划分为 4个区域

  1. 代码区:存放函数体的二进制代码,由操作系统进行管理
  2. 全局区:存放 全局变量、静态变量、全局常量、字符串常量
    常量区属于全局区,其存放 全局常量、字符串常量
  3. 栈区:由编译器自动分配释放,存放函数的参数值,局部变量、局部常量等
  4. 堆区:由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收
#include<iostream>
#include<string>
using namespace std;

//全局变量
int g_a = 10;// 全局区
int g_b = 10;

//全局常量
const int c_g_a = 10; // 常量区, 也是全局区
const int c_g_b = 10;

int main() {
	system("chcp 65001> nul");

	//静态变量
	static int s_a = 10; // 全局区
	static int s_b = 10;

	cout << "字符串常量地址为: " << (int)&"hello world" << endl; // 常量区, 也是全局区
	cout << "字符串常量地址为: " << (int)&"hello world1" << endl;

	//局部变量
	int a = 10; // 栈区
	int b = 10;

	// 局部常量
	const int c_l_a = 10;// 栈区
	const int c_l_b = 10;

	cout << "---------------------------------" << endl;

	// 栈区:由编译器自动分配释放, 存放函数的参数值, 局部变量等
	//		注:不要返回局部变量的地址,栈区开辟的数据由编译器自动释放
	// 堆区:由程序员分配释放, 若程序员不释放, 程序结束时由操作系统回收
	//		注:在C++中主要利用new在堆区开辟内存,用完后利用delete释放内存
	int* arr = new int[10];

	for (int i = 0; i < 10; i++) {
		arr[i] = i;
	}
	delete[] arr;//释放数组 delete 后加 [] 
	//cout << arr[0] << endl;// 报错:访问了已经释放的内存

	// 如果没有delete,程序运行期间,会发生内存泄漏,从而引发系统崩溃
	system("pause");
	return 0;
}

注:在程序编译后,生成了exe可执行程序,未执行该程序前分为 全局区、代码区 两个区域。

  • 代码区特点是共享和只读
  • 全局区的数据在程序结束后由操作系统释放

二、引用

2.1 引用定义和基本使用

引用:对变量的起一个别名,引用必须初始化,并且一旦初始化后就不能改变引用的对象。引用的语法更清楚简单,通过引用参数产生的效果同按地址传递是一样的

#include<iostream>
#include<string>
using namespace std;

// 函数声明
void swap01(int a, int b);
void swap02(int* a, int* b);
void swap03(int& a, int& b);

int main() {
	system("chcp 65001> nul");

	int c = 10;
	//int& d; // 错误:引用必须初始化
	int& d = c; // d是c的引用,d和c指向同一个内存地址
	//d = a;// 这是赋值操作,不是改变引用的对象,d仍然是c的引用,只不过c的值被修改了
	cout << "c的地址为: " << (int)&c << endl;
	cout << "d的地址为: " << (int)&d << endl; // 输出地址相同

	c = 20; // 修改c的值,d的值也会改变,因为它们指向同一个内存地址
	cout << "c的值为:" << c << endl;
	cout << "d的值为:" << d << endl;

	cout << "---------------------------------" << endl;

	int a = 10; // 栈区
	int b = 10;

	swap01(a, b);
	cout << "a:" << a << " b:" << b << endl; // 10,20

	swap02(&a, &b);
	cout << "a:" << a << " b:" << b << endl; // 交换成功 20,10

	swap03(a, b);
	cout << "a:" << a << " b:" << b << endl; // 交换成功 10, 20

	system("pause");
	return 0;
}

//1. 值传递
void swap01(int a, int b) {
	int temp = a;
	a = b;
	b = temp;
}

//2. 地址传递
void swap02(int* a, int* b) {
	int temp = *a;
	*a = *b;
	*b = temp;
}

//3. 引用传递
void swap03(int& a, int& b) {
	int temp = a;
	a = b;
	b = temp;
}

2.2 引用可以做返回值

引用可以做函数返回值,但不要返回局部变量引用,可以返回静态变量的引用。
另外,常量引用主要用来修饰形参,防止误操作。

#include<iostream>
#include<string>
using namespace std;

int& test01();
int& test02();

int main() {
	system("chcp 65001> nul");

	//不能返回局部变量的引用
	int& ref = test01();
	//cout << "ref = " << ref << endl; // 编译器不会报错,但运行时可能会出现错误,因为ref引用了一个已经被释放的内存地址

	// 可以返回静态变量的引用,因为静态变量存在全局区,不会被释放
	int& ref2 = test02();
	cout << "ref2 = " << ref2 << endl; // 20

	// 如果函数的返回值是一个引用,那么函数调用可以作为左值使用
	test02() = 1000; // 相当于 a = 1000, ref2又是a的引用,所以ref2的值也会改变
	cout << "ref2 = " << ref2 << endl; // 1000

	cout << "---------------------------------" << endl;

	//常量引用
	//常量引用主要用来修饰形参,防止误操作
	//void showValue(const int& v) {
	//	//v += 10; // 错误:常量引用不能修改引用的对象
	//}

	system("pause");
	return 0;
}

//返回局部变量引用
int& test01() {
	int a = 10; //局部变量
	return a;
}

//返回静态变量引用
int& test02() {
	static int a = 20; //静态变量,存在全局区,不会被释放
	return a;
}

2.3 引用的本质

引用的本质是指针常量,即指针指向不能修改,指针指向的内容可以修改。

int& ref = aa 相当于 int* const ref = &aa

#include<iostream>
#include<string>
using namespace std;

// 函数声明
void func(int& ref);

int main() {
	system("chcp 65001> nul");

	// 引用的本质:内部实现是一个指针常量
	int aa = 10;
	int& ref3 = aa; // 相当于 int* const ref3 = &aa,即指针常量,指针指向不能修改,指针指向的内容可以修改
	ref3 = 20;
	cout << aa << endl; // 20
	func(aa);
	cout << aa << endl; // 100

	system("pause");
	return 0;
}

void func(int& ref) { // int& ref = aa 相当于 int* const ref = &aa
	ref = 100; // ref是引用,转换为*ref = 100
	// 引用 = 自动取地址 + 自动解引用 + 地址不可修改的常量指针
}

三、函数提高

3.1 函数形参默认值与占位符

在C++中,函数的形参列表中的形参是可以有默认值的。

  1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
  2. 如果采用先声明再定义的方式,默认参数只写在「函数声明」
#include<iostream>

using namespace std;

// 1. 如果某个位置参数有默认值,那么从这个位置往后,从左向右,必须都要有默认值
// 2. 默认参数只写在「函数声明」
int func(int a, int b = 10, int c = 10);

// 函数占位参数,占位参数也可以有默认参数,调用函数时必须填补该位置
void func2(int a, int);

int main() {
	system("chcp 65001> nul");

	cout << "ret = " << func(20, 20) << endl; // 20 + 20 + 10
	cout << "ret = " << func(100) << endl; // 100 + 10 + 10

	func2(10, 10); //占位参数必须填补

	system("pause");
	return 0;
}

// 定义
int func(int a, int b, int c) {
	return a + b + c;
}

void func2(int a, int) {
	cout << "this is func2" << endl;
}

3.2 函数重载

函数重载:函数名相同,参数列表不同(个数不同,类型不同,顺序不同),和形参变量的名称以及返回值的类型无关。

#include<iostream>
#include<string>
using namespace std;

// 函数重载:函数名相同,参数列表不同(个数不同,类型不同,顺序不同),和形参变量的名称无关。
// 函数重载需要函数都在同一个作用域下
void func3();
void func3(int a);
void func3(double a);
void func3(int a, double b);
void func3(double a, int b);

int main() {
	system("chcp 65001> nul");
	
	// 函数重载
	func3();
	func3(10);
	func3(3.14);
	func3(10, 3.14);
	func3(3.14, 10);

	system("pause");
	return 0;
}

// 重载
void func3() {
	cout << "func3 的调用!" << endl;
}
void func3(int a) {
	cout << "func3 (int a) 的调用!" << endl;
}
void func3(double a) {
	cout << "func3 (double a)的调用!" << endl;
}
void func3(int a, double b) {
	cout << "func3 (int a ,double b) 的调用!" << endl;
}
void func3(double a, int b) {
	cout << "func3 (double a ,int b)的调用!" << endl;
}

注:函数重载需要函数都在同一个作用域下


3.3 引用作为重载条件,默认参数下的歧义

#include<iostream>
#include<string>
using namespace std;


// 引用作为重载条件
void func4(int& a);
void func4(const int& a);

// 重载遇到默认参数会产生歧义
void func5(int a, int b = 10);
void func5(int a);

int main() {
	system("chcp 65001> nul");

	// 引用作为重载条件
	int a = 10;
	func4(a); // func4 (int &a) 调用
	func4(10); // func4 (const int &a) 调用

	//func5(10); // 报错:调用 func5(int a) 还是 func5(int a, int b = 10) 都可以,产生歧义
	//int b = 10;
	//func5(b); // 报错,原因同上
	func5(10, 20); // func5(int a, int b = 10) 调用

	system("pause");
	return 0;
}

// 引用作为重载条件
void func4(int& a) {
	cout << "func4 (int &a) 调用 " << endl;
}

void func4(const int& a) {
	cout << "func4 (const int &a) 调用 " << endl;
}

// 重载遇到默认参数会产生歧义
void func5(int a, int b) {
	cout << "func5(int a, int b = 10) 调用" << endl;
}

void func5(int a) {
	cout << "func5(int a) 调用" << endl;
}

四、类和对象

4.1 封装

C++面向对象的三大特性为:封装、继承、多态,封装作为C++面向对象三大特性之一,其意义如下:

  • 将属性和行为作为一个整体,表现生活中的事物
  • 将属性和行为加以权限控制
权限关键字 本类内部 子类(派生类)内部 类外部(main / 普通函数) 特点总结
public ✅ 可以 ✅ 可以 ✅ 可以 完全公开,谁都能访问
protected ✅ 可以 ✅ 可以 ❌ 不可以 传子不传外,对外隐藏,对子类开放
private ✅ 可以 ❌ 不可以 ❌ 不可以 绝对私有,只有自己能访问
#include<iostream>
#include<string>

using namespace std;

class Person {
public:
	string m_Name;
protected:
	string m_Car;
private:
	int m_Password;
public:
	void func() {
		m_Name = "张三";
		m_Car = "拖拉机";
		m_Password = 123456;
	}
};

int main() {
	system("chcp 65001> nul");

	Person p;
	p.m_Name = "李四";
	//p.m_Car = "奔驰";  //保护权限类外访问不到
	//p.m_Password = 123; //私有权限类外访问不到
	
	system("pause"); // 程序卡死在这里,等待按键,按键后继续执行,执行完后程序结束,析构函数调用
	return 0;
}

注:

  1. 在C++中,struct和class唯一的区别就在于默认的访问权限不同。struct默认的访问权限是public,而class默认的访问权限是private。

  2. 通常设计成员为私有的,提供公共的成员函数来访问和修改私有成员,这样可以控制对数据的访问,保证数据的安全性和完整性

  3. C++ 和 Java在私有成员访问上是不同的
    C++ private:同类所有对象互相可见

    class Person {
    private:
        string name; // 私有成员
    public:
        Person(string n) : name(n) {}
    
        // 拷贝构造:直接访问另一个对象的私有name
        Person(const Person& p) {
            this->name = p.name; // ✅ C++ 允许
        }
    };
    

    Java private:只对自己可见,同类其他对象必须用 getter

    class Person {
        private String name;
        public Person(String n) { name = n; }
    
        // getter
        public String getName() { return name; }
    
        // 拷贝构造
        public Person(Person p) {
            // this.name = p.name; ❌ Java 报错!
            this.name = p.getName(); // ✅ Java 必须这样
        }
    }
    

4.2 对象的初始化和清理

构造函数语法:类名(){}

  1. 构造函数,没有返回值也不写void
  2. 函数名称与类名相同
  3. 构造函数可以有参数,因此可以发生重载
  4. 程序在调用对象时候会自动调用构造,无须手动调用,而且只会调用一次

析构函数语法: ~类名(){}

  1. 析构函数,没有返回值也不写void
  2. 函数名称与类名相同,在名称前加上符号 ~
  3. 析构函数不可以有参数,因此不可以发生重载
  4. 程序在对象销毁前会自动调用析构,无须手动调用,而且只会调用一次
#include<iostream>
#include<string>

using namespace std;

class Student {
public:
	Student() {
		
		cout << "Student构造函数调用" << endl;
	}
	~Student() {
		cout << "Student析构函数调用" << endl;
	}
};

int main() {
	system("chcp 65001> nul");

	Student s; // 构造函数调用

	system("pause"); // 程序卡死在这里,等待按键,按键后继续执行,执行完后程序结束,析构函数调用
	return 0;
}

4.3 构造函数的分类

两种分类方式:

  • 按参数分为: 有参构造和无参构造
  • 按类型分为: 普通构造和拷贝构造

三种调用方式:括号法、显示法、隐式转换法

#include<iostream>
#include<string>

using namespace std;

class Person {
public:
	//无参(默认)构造函数
	Person() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	Person(int a) {
		age = a;
		cout << "有参构造函数!" << endl;
	}
	//拷贝构造函数
	Person(const Person& p) { // 浅拷贝
		age = p.age;
		cout << "拷贝构造函数!" << endl;
	}
	//析构函数
	~Person() {
		cout << "析构函数!" << endl;
	}
private:
	int age;
};

int main() {
	system("chcp 65001> nul");
	
	// 1. 括号法,常用
	Person p1; //无参
	Person p2(10); //有参
	Person p3(p1); //拷贝
	//注意1:调用无参构造函数不能加括号
	
	// 2. 显示法
	Person p4 = Person();
	Person p5 = Person(10);
	Person p6 = Person(p4);
	//注意2:Person()单独一行写就是匿名对象  当前行结束之后,马上析构
	
	// 3. 隐式转换法
	Person p7 = 10; // 有参 Person p4 = Person(10); 
	Person p8 = p7; // 拷贝 Person p5 = Person(p4); 
	
	cout << "---------------------------------" << endl;
	
	//注意3:不能利用 拷贝构造函数 初始化匿名对象,编译器会认为是对象声明
	//Person p9(Person()); // 报错
	
	system("pause");
	return 0;
}

4.4 拷贝构造函数调用时机

拷贝构造函数调用时机通常有三种情况:

  1. 使用一个已经创建完毕的对象来初始化一个新对象
  2. 值传递的方式给函数参数传值
  3. 以值方式返回局部对象(理论上会调用拷贝构造,但多数情况编译器会做优化)
// 情况1 参考上文

// 情况2
void doWork(Person p1) {...}
void test02() {
	Person p;
	doWork(p); //这里会调用拷贝构造函数
}

// 情况3
Person doWork2(){
	Person p1;
	cout << (int*)&p1 << endl;
	return p1;
}
void test03() {
	Person p = doWork2(); // 这里会调用拷贝构造函数(理论上会)
}

4.5 深浅拷贝

浅拷贝:简单的 赋值拷贝 操作
深拷贝:在堆区重新申请空间,进行拷贝操作

默认情况下,c++编译器至少给一个类添加 3 个函数

  1. 默认构造函数(无参,函数体为空)
  2. 默认析构函数(无参,函数体为空)
  3. 默认拷贝构造函数,对属性进行值拷贝,即浅拷贝
#include<iostream>
#include<string>
using namespace std;

class Animal {
public:
	//无参(默认)构造函数
	Animal() {
		cout << "无参构造函数!" << endl;
	}
	//有参构造函数
	Animal(int age, int height) {
		cout << "有参构造函数!" << endl;
		m_age = age;
		m_height = new int(height);

	}
	//拷贝构造函数  
	Animal(const Animal& a) {
		cout << "拷贝构造函数!" << endl;
		//如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题
		m_age = a.m_age;
		m_height = new int(*a.m_height);
		//如果用浅拷贝,a1和a2的m_height指针指向同一块内存,析构时重复释放 → 程序崩溃
	}

	//析构函数
	~Animal() {
		cout << "析构函数!" << endl;
		if (m_height != NULL) {
			delete m_height;
		}
	}
private:
	int m_age;
	int* m_height;
};

int main() {
	system("chcp 65001> nul");
	
	Animal a1(18, 180);
	Animal a2(a1); // 调用深拷贝

	system("pause");
	return 0;
}

另外,构造函数调用规则如下:

  • 如果用户定义有参构造函数,c++不在提供默认无参构造,但是会提供默认拷贝构造
  • 如果用户定义拷贝构造函数,c++不会再提供其他构造函数

4.6 初始化列表

C++提供了初始化列表语法,用来初始化属性
语法:构造函数():属性1(值1),属性2(值2)... {}

#include<iostream>
#include<string>
using namespace std;

class Person {
public:
	//传统方式初始化
	//Person(int a, int b, int c) {
	//	m_A = a;
	//	m_B = b;
	//	m_C = c;
	//}

	//初始化列表方式初始化
	Person(int a, int b, int c) :m_A(a), m_B(b), m_C(c) {}
	void PrintPerson() {
		cout << "mA:" << m_A << endl;
		cout << "mB:" << m_B << endl;
		cout << "mC:" << m_C << endl;
	}
private:
	int m_A;
	int m_B;
	int m_C;
};

int main() {
	system("chcp 65001> nul");
	Person p(1, 2, 3);
	p.PrintPerson();
	system("pause");
	return 0;
}

4.7 类对象作为类成员

当类中成员是其他类对象时,我们称该成员为 对象成员。
构造的顺序:先调用对象成员的构造,再调用本类构造。析构顺序与构造相反。

#include<iostream>
#include<string>
using namespace std;

class Phone {
public:
	Phone(string name) {
		m_PhoneName = name;
		cout << "Phone构造" << endl;
	}

	~Phone() {
		cout << "Phone析构" << endl;
	}

	string m_PhoneName;
};

class Person {
public:
	//初始化列表可以告诉编译器调用哪一个构造函数
	Person(string name, string pName) :m_Name(name), m_Phone(pName) {
		cout << "Person构造" << endl;
	}

	~Person() {
		cout << "Person析构" << endl;
	}

	void playGame() {
		cout << m_Name << " 使用" << m_Phone.m_PhoneName << " 牌手机! " << endl;
	}

	string m_Name;
	Phone m_Phone;
};

int main() {
	system("chcp 65001> nul");

	Person p("张三", "苹果X");
	p.playGame();
	// Phone构造
	// Person构造
	// 张三 使用苹果X 牌手机!
	// Person析构
	// Phone析构
	
	system("pause");
	return 0;
}

4.8 静态成员

静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员,其分为:

  1. 静态成员变量
    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
  2. 静态成员函数
    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量
#include<iostream>
using namespace std;

class Person {
public:
	static int m_A; //静态成员变量
	static void func() {
		cout << "静态func调用" << endl;
		m_A = 100;
		//m_B = 100; //错误,不可以访问非静态成员变量
	}
private:
	int m_B;
	static int m_C; //静态成员变量也是有访问权限的
	static void func2() { 
		cout << "func2调用" << endl;
	}
};
int Person::m_A = 10; // 必须 类内声明,类外初始化

int main() {
	system("chcp 65001> nul");
	//静态成员变量和静态成员函数的两种访问方式

	//1、通过对象
	Person p1;
	p1.m_A = 100;
	p1.func();
	cout << "p1.m_A = " << p1.m_A << endl;

	Person p2;
	p2.m_A = 200;
	cout << "p1.m_A = " << p1.m_A << endl; //共享同一份数据
	cout << "p2.m_A = " << p2.m_A << endl;

	//2、通过类名
	cout << "m_A = " << Person::m_A << endl;
	Person::func();
	//cout << "m_C = " << Person::m_C << endl; //虽说是static,但是私有权限访问不到
	//Person::func2();

	system("pause");
	return 0;
}

五、C++对象模型和this指针

5.1 成员变量和成员函数分开存储

在C++中,类内的成员变量和成员函数分开存储,只有非静态成员变量才属于类的对象上。

#include<iostream>
#include<string>
using namespace std;

class Person {
public:
	Person() {
		mA = 0;
	}
	//非静态成员变量占对象空间
	int mA;
	//静态成员变量不占对象空间
	static int mB;
	//函数也不占对象空间,所有函数共享一个函数实例
	void func() {
	}
	//静态成员函数也不占对象空间
	static void sfunc() {
	}
};

int main() {
	system("chcp 65001> nul");

	cout << sizeof(Person) << endl; 
	// 输出4,Person类中只有一个非静态成员变量mA,占4个字节,静态成员变量和函数不占对象空间

	system("pause");
	return 0;
}

5.2 this指针

this 指针是隐含每一个非静态成员函数内的一种指针,主要作用如下:

  • 当形参和成员变量同名时,可用 this 指针来区分
  • 在类的非静态成员函数中返回对象本身,可使用 return *this
#include<iostream>
#include<string>
using namespace std;

class Animal {
public:
	Animal(int age) {
		this->age = age;
	}
	int getAge() {
		return age;
	}
private:
	int age;
};

int main() {
	system("chcp 65001> nul");

	Animal a(10);
	cout << "a.age = " << a.getAge() << endl;
	
	system("pause");
	return 0;
}

5.3 空指针访问成员函数

C++中空指针也是可以调用成员函数的,但是也要注意有没有用到 this 指针,如果用到 this 指针,需要加以判断保证代码的健壮性。

//空指针访问成员函数
class Person {
public:
	void ShowClassName() {
		cout << "我是Person类!" << endl;
	}

	void ShowPerson() {
		if (this == NULL) {
			return;
		}
		cout << mAge << endl;
	}

public:
	int mAge;
};
int main() {
	system("chcp 65001> nul");
  
	Person * p = NULL;
	p->ShowClassName(); //空指针,可以调用成员函数
	p->ShowPerson();  //但是如果成员函数中用到了this指针,就不可以了

	system("pause");
	return 0;
}

5.4 const修饰成员函数

常函数

  1. 成员函数后加 const 后我们称为这个函数为常函数
  2. 常函数内不可以修改成员属性
  3. 成员属性声明时加关键字 mutable 后,在常函数中依然可以修改

常对象

  1. 声明对象前加 const 称该对象为常对象
  2. 常对象只能调用常函数
#include<iostream>
using namespace std;

class Car {
public:
	Car() {
		m_A = 0;
		m_B = 0;
	}

	//如果想让指针指向的值也不可以修改,需要声明常函数
	void ShowPerson() const {
		//this = NULL; // 错误:this指针是一个指针常量,指针的指向不可修改
		//this->mA = 100; // 错误:常函数内不可以修改成员属性

		//const修饰成员函数,可修改mutable修饰的变量
		this->m_B = 100;
	}

	void MyFunc() const {
		//mA = 10000;
	}
public:
	int m_A;
	mutable int m_B; //可修改 可变的
};

int main() {
	system("chcp 65001> nul");

	const Car car; // 常对象
	cout << car.m_A << endl; // 0
	//car.m_A = 100; // 错误:常对象内成员属性不可修改
	car.m_B = 200; // 正确:car是常对象,m_B是mutable修饰的成员属性,在常函数内依然可以修改

	car.ShowPerson(); // 常对象只能调用常函数
	//car.MyFunc(); // 错误:常对象不能调用非常函数
	
	system("pause");
	return 0;
}
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐