1 2 3 4 5 6 7 8 9 10

【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 子类 : 继承方式 父类

继承方式

继承方式一共有三种:

  • 公共继承
  • 保护继承
  • 私有继承

一图流
image
注意:父类中私有成员也是被子类继承下去了,只是由编译器给隐藏后访问不到
继承中先调用父类构造函数,再调用子类构造函数,析构顺序与构造相反

父类作用域

例子

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 {};

原理如图
image

多态

比如你家有亲属结婚了,让你们家派个人来参加婚礼。
邀请函写的是让你爸来,但是实际上你去了,或者你妹妹去了,这都是可以的。
因为你们代表的是你爸,但是在你们去之前他们也不知道谁会去,只知道是你们家的人。可能是你爸爸,
可能是你们家的其他人代表你爸参加。这就是多态。

c++多态有以下几种:

  1. 重载。函数重载和运算符重载,编译期。
  2. 虚函数。子类的多态性,运行期。
  3. 模板,类模板,函数模板。编译期

静态多态和动态多态区别:

  • 静态多态的函数地址早绑定 - 编译阶段确定函数地址
  • 动态多态的函数地址晚绑定 - 运行阶段确定函数地址

就拿动物和猫举例
地址早绑定在编译阶段,确定函数地址
如果想执行让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,即地址晚绑定

在继承关系中,对于父类的方法我们也同样使用。
但是正常来说,我们希望方法的行为取决于调用方法的对象,而不是指针或引用指向的对象有关。

//函数前面加上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); //正确,如果用显示指定类型,可以发生隐式类型转换
}
posted @   mayoyi  阅读(14)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
点击右上角即可分享
微信分享提示