【C++学习笔记】一个先学了Java,Python,Csharp最后再来学C++的菜狗笔记
学到哪写到哪,就是这么随缘
我是黑马程序员~~
内存分区
1.代码区(程序运行前
存放程序中所有已编译好的机器指令,也就是 CPU 执行的二进制代码。该区域通常由操作系统或运行时环境加载并映射到内存。
共享且只读:
- 共享:代码区的内容可以被多个进程共享,这有助于提高系统效率和节省内存。例如,多个程序可以共享相同的库文件(如 DLL 或共享对象文件)。
- 只读:为了安全性和避免程序运行时修改自身代码,代码区通常是只读的。这样可以防止程序错误地修改代码段的内容,导致潜在的程序崩溃或漏洞。
2.全局区(程序运行前
存放全局变量,静态变量,全局常量,字符串常量
3.栈内存区(程序运行后
由编译器自动分配释放,存放函数的参数值,局部变量等
因为是由编辑器自动分配和回收,所以不能返回形参,局部变量的地址
4.堆内存区(程序运行后
由程序员分配释放,程序结束后由操作系统释放
利用new可以把数据开辟到堆区
int * p = new int(10)
5.需要注意的
与csharp不同的是,只有用new方法创建的东西才是放在堆内存的
即使是你认为的csharp的"引用类型/对象类型"也是能作为一个值存在栈内存的
你可以通过 int* i = new int(10);
也可以直接 Person p; //此时已经以默认的构造函数创建出来了
sizeof
sizeof 是一个编译时运算符,用来返回一个数据类型或对象在内存中所占用的字节数。
1.基本
sizeof(int)
返回 4 字节
sizeof(char)
返回 1 字节,按照 C++ 标准,char 类型的大小始终是 1 字节。
sizeof(double)
通常返回 8 字节。
2.数组
sizeof对于数组会返回整个数组的大小(即数组中所有元素的总字节数)
而对于数组指针,则返回指针本身所占的字节数。
需要注意的是,函数传递数组就是数组指针
例如:
void func(int arr[]) { //此时输出就是4 std::cout << "sizeof(arr) = " << sizeof(arr) << std::endl; }
3.对于STL
sizeof得到的是
字符串
1.char数组
char str[] = "hello world";
可以使用cstring 库中的函数(如 strlen, strcpy)。
2.string类型
#include<string>
string str = "hello world";
与csharp,java等语言不同的是
动态分配内存,由标准库管理。
支持操作符重载(如 +, == 等)。
std::string 是可变的,类似 StringBuilder
3.杂项
如果控制台输出的是乱码可以加下面这一行
SetConsoleOutputCP(CP_UTF8); // 设置控制台输出为 UTF-8 编码
指针
指针本质其实就是记录内存地址
值得注意的一点:是在 C++ 中,如果不使用指针和引用,默认情况下都是值传递
for (type& element : container) { //加上&表示引用,如果去掉的话每次迭代都会创建新的拷贝 }
1.指针使用
int a = 233; int * p; p = &a; //将p指针指向a这块内存 *p = 666; //解引用,此时a也会变成666
2.指针大小
无论什么类型的指针统一都是固定大小。
在32位操作系统下: 占用4个字节空间,64位下占8个字节。
3.const修饰指针
常量指针
例如:const int *p = &a;
指针的指向可以修改,但是指针指向的值不可以修改
指针常量
例如 int * const p = &a
指针的指向不可以修改,但是指针指向的值可以修改
常量指针常量
例如 const int * const p = &a
两个都不可以修改
4.指针类型
空指针
空指针指向内存中编号为0的空间
一般用于初始化指针变量
空指针不能够进行访问
例如
int * p = NULL;
*p = 1;
或者 cout << *p <<endl;
那么此时就会报错,因为0-255号内存都是系统占用的,不可以访问
野指针
野指针指向非法的内存空间
例如
int * p = (int *)0X1100
也是会报错,因为这块内存空间不是自己申请的,没有访问权限
引用
可以看作是已存在变量的另一种名称。
引用与指针类似,但它有一些独特的特性和用途。
&别名 = 原名
注意:
- 引用必须初始化。
- 引用一旦绑定,不能再指向其他变量。
引用的本质
本质其实就是就一个隐式的指针常量。
这同时也说明了为什么引用不可以更改他的指向。
指针和引用的使用对比
int a = 10, b = 20; //使用引用 int& ref = a; ref = 30; //修改 ref 会修改 a // 使用指针 int* ptr = &a; *ptr = 40; //修改 *ptr 会修改 a ptr = &b; //ptr 现在指向 b
底层代码示意
int a = 10; int& ref = a; ref = 20; //上面的可以等价于下面 int a = 10; int* const ref = &a; // 引用是一个隐式的、不可更改的指针 *ref = 20; // 操作引用相当于通过指针操作原变量
引用的主要用途
函数传参(避免拷贝,提高性能)
使用引用传递参数时,函数操作的是原始对象,而不是其副本。
void mySwap(int& a,int& b) { //使用引用 //修改的是原变量 int temp = a; a = b; b = temp; } int a = 10; int b = 20; mySwap(a,b) std::cout << a << std::endl; //输出 20 std::cout << b << std::endl; //输出 10
常量引用(防止修改,适用于大对象)
常量引用允许通过引用传递参数,但禁止在函数中修改参数。
void print(const std::string& str) { std::cout << str << std::endl; } std::string s = "Hello"; print(s); // 通过常量引用传递,避免拷贝,提高效率
函数返回值的引用
函数可以返回引用,用于返回局部变量以外的对象。
int& getElement(int arr[], int index) { return arr[index]; // 返回数组元素的引用 } int arr[5] = {1, 2, 3, 4, 5}; getElement(arr, 2) = 10; // 修改数组元素 std::cout << arr[2] << std::endl; // 输出 10
需要注意的是,不要返回局部变量的引用,输出两次后可能导致不可预料的行为
int& get() { int a = 10; return a; // 返回局部变量的引用 } int &ref = get(); std::cout << ref << std::endl; // 输出 10 std::cout << ref << std::endl; // 输出 乱码
类和对象
空对象的大小也是1字节
这个字节的作用是区分空对象所占的位置
与结构体的区别
在C++中,类对象与结构体唯一的区别就是默认访问权限的不同
结构体默认为public
类对象默认为private
不过在实际使用中的选择
如果你需要表示一个简单的数据结构并且不打算进行封装,推荐使用 struct。
如果你需要更严格的封装、控制数据访问、支持继承和多态,推荐使用 class。
构造函数和析构函数
当类中存在其他类对象时
构造的顺序是 :先调用对象成员的构造,再调用本类构造
析构顺序则与构造相反
class Person { public: //构造函数 Person() { cout << "Person的构造函数调用" << endl; } //析构函数 ~Person() { cout << "Person的析构函数调用" << endl; } };
拷贝构造函数
一个比较特殊的东西
Person(const Person& p) { cout << "拷贝构造函数!" << endl; mAge = p.mAge; }
C++中拷贝构造函数调用时机通常有三种情况
- 使用一个已经创建完毕的对象来初始化一个新对象
- 值传递的方式给函数参数传值
- 以值方式返回局部对象
深拷贝和浅拷贝
浅拷贝本质其实就是在拷贝的过程中
导致了两个对象里的成员指针指向了同一块堆内存
深拷贝则是申请一块新的内存
重写拷贝构造函数即可解决
Person(const Person& p) { cout << "拷贝构造函数!" << endl; //如果不利用深拷贝在堆区创建新内存,会导致浅拷贝带来的重复释放堆区问题 m_age = p.m_age; m_height = new int(*p.m_height); }
初始化列表
直接举例
传统方式初始化 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) {}
静态成员
静态成员就是在成员变量和成员函数前加上关键字static,称为静态成员
C++中,类内的成员变量和成员函数分开存储
只有非静态成员变量才属于类的对象上
静态成员变量
- 所有对象共享同一份数据
- 在编译阶段分配内存
- 类内声明,类外初始化
静态成员函数
- 所有对象共享同一个函数
- 静态成员函数只能访问静态成员变量
可以通过类对象直接访问静态成员
也可以通过::进行作用域解析来访问
func是静态函数 //通过对象 Person p1; p1.func(); //通过类名 Person::func();
这其实也是为什么要用using namespace std;
的原因
不然cout就得写成std::cout << "123";
需要注意的是,静态成员只能在类外初始化
例如:int MyClass::myStatic = 10; //在类的外部赋值
或者自己类内静态函数初始化
友元
友元的目的是让一个函数或者类 访问另一个类中私有成员
友元的关键字为 friend
友元的三种实现
- 全局函数做友元
- 类做友元
- 成员函数做友元
//Building类内部 //告诉编译器 goodGay全局函数 是 Building类的好朋友,可以访问类中的私有内容 friend void goodGay(Building * building); //告诉编译器 goodGay类是Building类的好朋友,可以访问到Building类中私有内容 friend class goodGay; //告诉编译器 goodGay类中的visit成员函数 是Building好朋友,可以访问私有内容 friend void goodGay::visit(); //静态的也一样
const修饰成员
常函数:
成员函数后加const后我们称为这个函数为常函数
常函数内不可以修改成员属性
成员属性声明时加关键字mutable后,在常函数中依然可以修改
常对象:
声明对象前加const称该对象为常对象
常对象只能调用常函数
const Person person; //常对象
运算符重载
//成员函数实现 + 号运算符重载 Person operator+(const Person& p) { Person temp; temp.m_A = this->m_A + p.m_A; temp.m_B = this->m_B + p.m_B; return temp; }
继承
继承的语法:class 子类 : 继承方式 父类
继承方式
继承方式一共有三种:
- 公共继承
- 保护继承
- 私有继承
一图流
注意:父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到
继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反
父类作用域
例子
Son s; cout << "Son下的m_A = " << s.m_A << endl; cout << "Base下的m_A = " << s.Base::m_A << endl; s.func(); s.Base::func();
注意当子类与父类拥有同名的成员函数
子类会隐藏父类中同名成员函数
加作用域可以访问到父类中同名函数
静态也一样
cout << "Son 下 m_A = " << Son::A << endl; //A是静态成员 cout << "Base 下 m_A = " << Son::Base::A << endl; Son::func(); Son::Base::func();
多继承
C++允许一个类继承多个类
语法:class 子类 :继承方式 父类1 , 继承方式 父类2...
多继承可能会引发父类中有同名成员出现,需要加作用域区分
C++实际开发中不建议用多继承
cout << "sizeof Son = " << sizeof(s) << endl; cout << s.Base1::m_A << endl; cout << s.Base2::m_A << endl;
菱形继承问题
羊继承了动物的数据,驼同样继承了动物的数据,当草泥马使用数据时,就会产生二义性。
草泥马继承自动物的数据继承了两份,其实我们应该清楚,这份数据我们只需要一份就可以。
//继承前加virtual关键字后,变为虚继承 //此时公共的父类Animal称为虚基类 class Sheep : virtual public Animal {}; class Tuo : virtual public Animal {}; class SheepTuo : public Sheep, public Tuo {};
原理如图
多态
比如你家有亲属结婚了,让你们家派个人来参加婚礼。 邀请函写的是让你爸来,但是实际上你去了,或者你妹妹去了,这都是可以的。 因为你们代表的是你爸,但是在你们去之前他们也不知道谁会去,只知道是你们家的人。可能是你爸爸, 可能是你们家的其他人代表你爸参加。这就是多态。
c++多态有以下几种:
- 重载。函数重载和运算符重载,编译期。
- 虚函数。子类的多态性,运行期。
- 模板,类模板,函数模板。编译期
静态多态和动态多态区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
就拿动物和猫举例
地址早绑定在编译阶段,确定函数地址
如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,即地址晚绑定
在继承关系中,对于父类的方法我们也同样使用。
但是正常来说,我们希望方法的行为取决于调用方法的对象,而不是指针或引用指向的对象有关。
//函数前面加上virtual关键字,变成虚函数,那么编译器在编译的时候就不能确定函数调用了。 virtual void speak() { cout << "动物在说话" << endl; }
纯虚函数%抽象类
C++ 中没有像Java或C#中那样专门的接口(Interface)类型,但是可以通过一些技术手段来实现类似接口的功能。
实际上,C++ 中的接口通常是通过抽象类(也叫纯虚类)来实现的。
public: //纯虚函数 //类中只要有一个纯虚函数就称为抽象类 //抽象类无法实例化对象 //子类必须重写父类中的纯虚函数,否则也属于抽象类 virtual void func() = 0;
Base * base = NULL; //base = new Base; // 错误,抽象类无法实例化对象 base = new Son; base->func(); delete base;//记得销毁
虚析构和纯虚析构
多态使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类的析构代码
解决方式:将父类中的析构函数改为虚析构或者纯虚析构
虚析构语法:
virtual ~类名(){}
纯虚析构语法:
virtual ~类名() = 0;
类名::~类名(){}
模版
其实就是泛型
//利用模板提供通用的交换函数 template<typename T> void mySwap(T& a, T& b) { T temp = a; a = b; b = temp; } int a = 10; int b = 20; char c = 'c'; mySwap(a, b); // 正确,可以推导出一致的T //mySwap(a, c); // 错误,推导不出一致的T类型
隐式转换
//普通函数 int myAdd01(int a, int b) { return a + b; } //函数模板 template<class T> T myAdd02(T a, T b) { return a + b; } //使用函数模板时,如果用自动类型推导,不会发生自动类型转换,即隐式类型转换 void test01() { int a = 10; int b = 20; char c = 'c'; cout << myAdd01(a, c) << endl; //正确,将char类型的'c'隐式转换为int类型 'c' 对应 ASCII码 99 //myAdd02(a, c); // 报错,使用自动类型推导时,不会发生隐式类型转换 myAdd02<int>(a, c); //正确,如果用显示指定类型,可以发生隐式类型转换 }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!