c++面向对象
C++ 中面向对象的编程技术
整理了之前的笔记,补档。
1. 内存分区模型
C++ 在执行时将内存大方向划分为四个区域:
1) 程序执行前
代码区
- 存放 CPU 执行的机器指令,由操作系统进行管理。
- 代码区是共享的,可以被反复执行。
- 代码区是只读的,防止程序意外修改指令。
全局区
- 存放全局变量和静态变量(函数体外的变量是全局变量)。
- 包含常量区,字符串常量和其他常量(包括
const
修饰的变量)。 - 该区域的数据在程序结束后由操作系统释放。
2) 程序执行后
栈区
- 由编译器自动分配和释放,存放函数的参数值、局部变量等。
- 注意:不要在函数内部返回局部变量的地址,栈区开辟的数据由编译器自动释放。
堆区
-
由程序员分配和释放,若程序员不释放,程序结束时由操作系统回收。
-
意义:不同区域存放的数据,赋予不同的生命周期,给我们更大的灵活性。
-
利用
new
关键字可以将数据开辟到堆区:int* p = new int(10); // 用指针保存位置 新创建的对象是一个指针,指针本质也是局部变量,存放在栈区,而指针所指向的数据存放在堆区。
3) new
操作符
-
利用
delete
释放内存。-
创建语法:
int* p = new int(10);
new
返回的数据类型是指针,不会自动释放内存。 -
释放语法:
delete p;
释放后再次访问即为非法操作。
-
-
在堆区利用
new
开辟数组:-
语法:
int* p = new int[10];
-
释放:
delete[] p; // 数组时要加上中括号
-
2. 引用
引用的本质
-
引用是给变量起别名,通过不同的变量名来操作同一块内存(两个变量的地址相同)。引用本质上是一个指针常量。
-
语法:
数据类型& 别名 = 原名;
引用的注意事项
- 引用必须初始化,不能先创建后引用。
- 引用一旦初始化后不能更改。
引用传递
-
引用作为函数参数,利用引用使形参修饰实参,简化指针修改实参:
int swap(int &a, int &b) { // 交换操作 }
引用作为函数返回值
int& func() {
static int a = 10;
return a;
}
int &b = func(); // 直接使用引用作为返回值
-
函数调用可以作为左值:
func() = 100; // 直接给函数返回值赋值
常量引用
-
使用
const
修饰形参,防止误操作。加入const
后,编译器将代码修改为:int temp = 10; const int& a = temp; // 引用了一块临时的空间
3. 函数提高
① 函数默认参数
- 函数形参可以有默认值,调用时如果不传值则使用默认值。
- 注意:
- 如果某个位置有了默认值,从此位置开始从左到右的所有参数都必须有默认值。
- 如果函数声明有默认参数,函数的实现中不能再有默认值。
② 函数占位参数
- 只写数据类型,不写形参变量名,占位参数可以有默认值。
③ 函数重载
- 函数名可以相同,增加代码复用性,依据参数
类型
、个数
或顺序
的不同,调用不同的函数。 - 注意:
- 引用作为重载的条件:
int&
和const int&
是不同类型。 - 函数重载可能会遇到二义性,避免重载函数中包含默认参数。
- 引用作为重载的条件:
4. 类和对象
C++ 面向对象的三大特性:封装、继承、多态。
A. 封装
- 将属性和行为作为一个整体,并对其进行权限控制:
- 公共权限 (
public
):类内外都可以访问。 - 保护权限 (
protected
):类内可以访问,类外不可以访问,子类可以访问父类内容。 - 私有权限 (
private
):类内可以访问,类外不可以访问,子类无法访问父类的私有成员。
- 公共权限 (
示例代码:
class Circle {
public:
int m_r;
double zhouchang() {
return 2 * 3.14 * m_r;
}
private:
// 私有成员
};
struct
和 class
的区别
struct
默认权限是公共 (public
),class
默认权限是私有 (private
)。
类外调用类内对象时要表明作用域。
B. 对象的初始化和清理
构造函数
- 主要作用是创建对象时为对象的成员属性赋值。
- 无返回值,不写
void
。 - 构造函数名称与类名相同。
- 可以重载,有参构造、无参构造和拷贝构造。
- 程序在调用对象时候由编译器自动调用,无需手动调用。
构造函数的分类:无参构造(默认构造);有参构造;
普通构造
拷贝构造用引用的方式传入函数,将传入的人身上的所有属性拷贝到我身上。
调用方法
括号法 (比较常用)
类名 类变量名; //默认构造
类名 类变量名(函数参数)//有参构造函数
类名 类变量名(要拷贝的类变量名) //拷贝构造函数
注意:调用默认构造函数时 不要加(),因为编译器会认为这是函数声明。
显示法
类名 类变量名 = 类名(参数); //有参构造函数,右侧是一个匿名对象,操作结束后系统自动回收。
类名 类变量名 = 类名(要拷贝的类变量名); //拷贝构造函数
注意:不要利用拷贝构造函数,来初始化匿名对象。编译器会认为这是一个对象声明,person (p3)
等价于 person p3
;
隐式转换法
类名 类变量名 = 参数; //有参构造函数
类名 类变量名 = 要拷贝的类变量名; //拷贝构造函数
省略了类名
拷贝构造函数的调用时机
使用一个已经创建的对象来初始化一个新对象
值传递的方式给函数参数传值
以值方式返回局部变量
创建方法:
类名 (const 类名&变量名){函数体;}
析构函数
对象销毁前系统自动调用,执行一些清理工作。
~类名(){ }
-
没有返回值也不写void。
-
函数名称与类名相同,在名称前加上~
-
析构函数不可以有参数,因此不可以发生重载
-
自行调用,而且只会调用一次
作用:将堆区开辟的数据做释放操作。如果用new在堆区开辟内存,则要在析构函数中用delete手动释放内存。
深拷贝与浅拷贝
- 浅拷贝:简单的赋值拷贝,编译器默认的。如果浅拷贝可能会造成堆区内存重复释放,
- 深拷贝:通过手动实现拷贝构造函数,避免浅拷贝造成内存泄漏。在堆区重新申请空间,进行拷贝操作,通过自己实现拷贝构造函数,来进行深拷贝
初始化列表
-
传统初始化:在构造函数体内进行。
-
初始化列表:构造函数初始化时通过列表直接初始化类成员。
-
语法:
构造函数(数据类型 值1,数据类型 值2……):类变量1(值1),类变量2(值2)……{ }
注意冒号的位置
类对象作为类成员
类中的成员可以是另外一个类的对象,该对象为对象成员
当出现这种情况时,构造时先构造对象,再构造类本身。
析构的顺序和构造的顺序相反
静态成员
静态成员变量
在成员函数和成员变量前加上关键字static
,称为静态成员
- 所有对象共享同一份数据,在编译阶段分配内存。
- 类内声明,类外初始化。
class person{
public:
static int m_a; //类内声明
}
int person::m_a = 10; //类外初始化
静态成员变量不属于某个对象,所有对象共享同一份数据。
访问方式:person::m_a 通过类名访问
Ø 静态成员变量和静态成员函数也是有访问权限的。
静态成员函数
-
所有对象共享同一个函数,只能访问静态成员变量。
-
根据你的要求,以下是C++对象模型和多态部分的内容,保持你的内容不变并进行补充:
根据你的要求,以下是C++对象模型和多态部分的内容,保持你的内容不变并进行补充:
C. 对象模型和 this
指针
类内的成员变量和成员函数分开存储
- C++ 中,类的成员变量和成员函数分别存储。成员变量通常存储在对象的内存中,而成员函数存储在程序的代码区中(即共享内存区域)。
- 只有非静态成员变量才属于类的对象上(即每个对象都会有自己的成员变量副本)。静态成员变量属于类本身,而不是某个特定对象。
空对象占用的字节
- 空对象占用 1 字节,因为 C++ 编译器会给每个空对象分配一个字节的空间,以区分空对象在内存中的位置。即使对象没有成员变量,仍然会占用最少的内存空间。
- 可以通过
sizeof()
来计算对象所占用的内存空间。sizeof()
计算的是对象的总大小,包括成员变量、内存对齐等。
示例:
class Empty {
// 没有成员变量
};
int main() {
Empty e;
cout << sizeof(e) << endl; // 输出 1
return 0;
}
this
指针
this
指针是 C++ 中的一个特殊指针,它指向调用成员函数的对象。- 每个对象在调用成员函数时,编译器会自动将该对象的地址作为
this
指针传递给成员函数。this
是一个常量指针,不能修改它指向的对象。 - 友元函数没有
this
指针,因为它们不是类的成员函数。
this
指针的用途
- 解决成员变量与函数参数名称冲突:可以通过
this->
来区分成员变量和函数参数。 - 返回对象本身:通过
*this
解引用返回当前对象的引用,通常用于实现链式调用。
示例:
class Person {
public:
int age;
// 通过返回 *this 来支持链式调用
Person& addAge(int years) {
this->age += years;
return *this; // 返回对象本身
}
};
int main() {
Person p;
p.age = 20;
p.addAge(5).addAge(3); // 链式调用
cout << p.age << endl; // 输出 28
return 0;
}
空指针调用成员函数
- 空指针(
NULL
或nullptr
)可以调用成员函数,但在访问成员函数时,程序会崩溃。 - 为了避免空指针引用错误,可以在成员函数内部添加空指针检查。
示例:
class Person {
public:
int age;
void printAge() {
if (this == nullptr) {
return; // 防止空指针调用
}
cout << "Age: " << age << endl;
}
};
int main() {
Person* p = nullptr;
p->printAge(); // 安全处理,避免崩溃
return 0;
}
const
修饰成员函数
- 常函数(
const
成员函数):在成员函数的后面加上const
,表明该函数不会修改类的成员变量。 - 在常函数中,不能修改成员变量,但可以访问
mutable
修饰的成员变量。
示例:
class Person {
public:
mutable int age;
void setAge(int a) const { // 常函数
age = a; // `mutable` 允许修改成员
}
};
int main() {
const Person p;
p.setAge(25); // 调用常函数
cout << p.age << endl; // 输出 25
return 0;
}
D. 友元
友元函数
- 通过
friend
关键词,可以让类外的函数访问类的私有成员。通常用于访问类的内部数据,尤其是操作类对象时。
示例:全局函数作友元
class Building {
friend void printBedroom(Building& b); // 友元函数声明
private:
string m_Bedroom;
public:
Building() : m_Bedroom("Master Bedroom") {}
};
void printBedroom(Building& b) {
cout << "Bedroom: " << b.m_Bedroom << endl;
}
int main() {
Building b;
printBedroom(b); // 通过友元函数访问私有成员
return 0;
}
友元类
- 可以将一个类声明为另一个类的友元类,使得友元类能够访问该类的私有成员。
示例:类作友元
class Building {
friend class GoodFriend; // 友元类声明
private:
string m_Bedroom;
public:
Building() : m_Bedroom("Master Bedroom") {}
};
class GoodFriend {
public:
void showBedroom(Building& b) {
cout << "Bedroom: " << b.m_Bedroom << endl;
}
};
int main() {
Building b;
GoodFriend gf;
gf.showBedroom(b); // 通过友元类访问私有成员
return 0;
}
友元函数作为成员函数
- 可以指定某个类的成员函数作为另一个类的友元函数,使得该函数可以访问类的私有成员。
示例:成员函数作友元
class Building {
friend void GoodFriend::visit(Building& b); // 友元函数声明
private:
string m_Bedroom;
public:
Building() : m_Bedroom("Master Bedroom") {}
};
class GoodFriend {
public:
void visit(Building& b) {
cout << "Visiting Bedroom: " << b.m_Bedroom << endl;
}
};
int main() {
Building b;
GoodFriend gf;
gf.visit(b); // 通过友元函数访问私有成员
return 0;
}
E. 运算符重载
运算符重载允许我们为已有的运算符定义新的行为,使其适应新的数据类型。这样可以使代码更加简洁和易于理解。
常见的运算符重载
- 算术运算符:
+
,-
,*
,/
,%
- 关系运算符:
==
,!=
,<
,>
,<=
,>=
- 逻辑运算符:
&&
,||
,!
- 自增自减运算符:
++
,--
- 赋值运算符:
=
,+=
,-=
- 输入输出运算符:
<<
,>>
1. 重载 +
运算符
通过运算符重载,可以简化对象之间的加法操作。
示例:成员函数重载 +
运算符
class Person {
public:
int age;
Person operator+ (const Person& p) { // 运算符重载
Person temp;
temp.age = this->age + p.age;
return temp;
}
};
int main() {
Person p1, p2, p3;
p1.age = 10;
p2.age = 20;
p3 = p1 + p2; // 使用重载的 + 运算符
cout << p3.age << endl; // 输出 30
return 0;
}
示例:全局函数重载 +
运算符
class Person {
public:
int age;
};
Person operator+ (const Person& p1, const Person& p2) {
Person temp;
temp.age = p1.age + p2.age;
return temp;
}
int main() {
Person p1, p2, p3;
p1.age = 10;
p2.age = 20;
p3 = p1 + p2; // 使用重载的 + 运算符
cout << p3.age << endl; // 输出 30
return 0;
}
2. 重载输入输出运算符 (<<
, >>
)
- 通过重载
<<
运算符,可以自定义输出类型的方式,使得可以使用cout
输出自定义类的对象。 - 通过重载
>>
运算符,可以自定义输入类型的方式,使得可以使用cin
输入自定义类的对象。
示例:重载 <<
运算符
class Person {
public:
int age;
friend ostream& operator<<(ostream& os, const Person& p) {
os << "Age: " << p.age;
return os;
}
};
int main() {
Person p;
p.age = 25;
cout << p << endl; // 输出
"Age: 25"
return 0;
}
3. 重载自增运算符 (++
)
- 重载自增运算符可以是前置自增或后置自增。前置自增返回对象本身,后置自增返回对象的副本。
示例:重载自增运算符
class MyInteger {
public:
int num;
MyInteger() : num(0) {}
MyInteger& operator++() { // 前置自增
num++;
return *this;
}
MyInteger operator++(int) { // 后置自增
MyInteger temp = *this;
num++;
return temp;
}
};
int main() {
MyInteger m;
++m; // 前置自增
cout << m.num << endl; // 输出 1
m++; // 后置自增
cout << m.num << endl; // 输出 2
return 0;
}
4. 重载赋值运算符 (=
)
- 默认情况下,编译器提供浅拷贝的赋值运算符,适用于简单类型。但对于涉及动态内存管理的类,需要手动重载赋值运算符进行深拷贝。
示例:重载赋值运算符
class Person {
public:
int* age;
Person(int a) {
age = new int(a);
}
// 赋值运算符重载(深拷贝)
Person& operator=(const Person& p) {
if (this == &p) return *this; // 防止自赋值
delete age; // 释放原有内存
age = new int(*p.age); // 进行深拷贝
return *this;
}
~Person() {
delete age; // 释放内存
}
};
int main() {
Person p1(25);
Person p2(30);
p2 = p1; // 使用重载的赋值运算符进行深拷贝
cout << *p2.age << endl; // 输出 25
return 0;
}
运算符重载的注意事项:
- 重载运算符时需要保持运算符的语义一致性。
- 不要滥用运算符重载,尤其是对于内置数据类型的运算符,避免混淆程序的逻辑。
5. 多态和虚函数
动态多态
动态多态是面向对象编程中最重要的特性之一,它允许程序在运行时决定调用哪一个版本的函数。当一个基类的指针或引用指向派生类的对象时,就可以通过虚函数实现多态行为。
关键点:
- 继承关系:动态多态要求有继承关系,子类需要重写父类的虚函数。
- 虚函数:父类中的虚函数必须被子类重写(覆盖),从而允许运行时动态绑定。
纯虚函数与抽象类
-
纯虚函数:在父类中声明为
= 0
,表示该函数没有实现,必须由派生类实现。- 纯虚函数使得类变为抽象类,抽象类无法直接实例化,只能通过其派生类来使用。
纯虚函数语法:
virtual 返回值类型 函数名 (参数列表) = 0;
当类里有了纯虚函数,这个类也就被称为抽象类。
特点:①无法实例化对象;②子类必须重写抽象类里的纯虚函数否则也是抽象类
纯虚函数示例:
class Animal {
public:
virtual void speak() = 0; // 纯虚函数,必须被派生类重写
};
class Dog : public Animal {
public:
void speak() override {
cout << "Woof!" << endl;
}
};
class Cat : public Animal {
public:
void speak() override {
cout << "Meow!" << endl;
}
};
void makeSound(Animal& animal) {
animal.speak();
}
int main() {
Dog dog;
Cat cat;
makeSound(dog); // 输出 Woof!
makeSound(cat); // 输出 Meow!
return 0;
}
6. 虚析构函数
虚析构函数用于确保当通过基类指针删除派生类对象时,派生类的析构函数被正确调用。这是实现多态的一部分,如果没有虚析构函数,程序可能会发生内存泄漏,因为基类析构函数不会自动调用派生类的析构函数。
虚析构函数示例:
class Base {
public:
virtual ~Base() {
cout << "Base class destructor" << endl;
}
};
class Derived : public Base {
public:
~Derived() override {
cout << "Derived class destructor" << endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 删除派生类对象时会先调用派生类的析构函数,再调用基类的析构函数
return 0;
}
7. C++ 中的模板
模板是 C++ 中实现泛型编程的工具,可以让代码更具通用性,减少重复编写。
函数模板
函数模板允许我们编写一个函数模板,之后可以根据传入的类型自动推导出相应类型的函数。
函数模板示例:
template <typename T>
T add(T a, T b) {
return a + b;
}
int main() {
cout << add(3, 4) << endl; // 传入整数
cout << add(3.5, 4.5) << endl; // 传入浮点数
return 0;
}
类模板
类模板允许我们编写一个类模板,之后可以使用不同类型实例化该类。
类模板示例:
template <typename T>
class Box {
private:
T value;
public:
Box(T v) : value(v) {}
T getValue() { return value; }
};
int main() {
Box<int> intBox(10);
Box<double> doubleBox(3.14);
cout << intBox.getValue() << endl;
cout << doubleBox.getValue() << endl;
return 0;
}
8. C++ 的多重继承与虚继承
C++ 允许多重继承,即一个类可以继承多个父类。然而,多重继承有时会导致问题,特别是在父类中有相同成员时,这种情况被称为菱形继承。为了解决菱形继承问题,C++ 引入了虚继承。
菱形继承问题
菱形继承指的是一个类继承自两个类,而这两个类又有相同的基类。在没有虚继承的情况下,派生类会有两个基类的副本,导致二义性问题和资源重复管理的问题。
菱形继承示例:
class A {
public:
void show() {
cout << "Class A" << endl;
}
};
class B : public A {
public:
void show() {
cout << "Class B" << endl;
}
};
class C : public A {
public:
void show() {
cout << "Class C" << endl;
}
};
class D : public B, public C {
// D 类从 B 和 C 类继承,且 B 和 C 都继承自 A
};
int main() {
D d;
d.show(); // 这里会有二义性,因为 D 类继承了两个 A 类的副本
return 0;
}
虚继承
虚继承解决了菱形继承问题,使得类 D
只有一个 A
的副本,而不是两个。在虚继承中,父类 A
的构造函数只会调用一次,派生类 B
和 C
不会重复调用 A
的构造函数。
虚继承示例:
class A {
public:
A() { cout << "A constructor" << endl; }
virtual void show() { cout << "Class A" << endl; }
};
class B : virtual public A {
public:
void show() override { cout << "Class B" << endl; }
};
class C : virtual public A {
public:
void show() override { cout << "Class C" << endl; }
};
class D : public B, public C {
public:
void show() override { cout << "Class D" << endl; }
};
int main() {
D d;
d.show(); // 输出 "Class D"
return 0;
}
9. C++ 的智能指针
智能指针是 C++11 引入的,用来管理动态内存的分配和释放,避免手动内存管理导致的内存泄漏问题。常见的智能指针有 std::unique_ptr
、std::shared_ptr
和 std::weak_ptr
。
std::unique_ptr
std::unique_ptr
是一种独占所有权的智能指针,确保同一时刻只有一个 unique_ptr
拥有资源的所有权。
std::unique_ptr
示例:
#include <memory>
class Person {
public:
Person() { cout << "Person created" << endl; }
~Person() { cout << "Person destroyed" << endl; }
};
int main() {
std::unique_ptr<Person> p1 = std::make_unique<Person>(); // 自动管理内存
// 不需要手动调用 delete,p1 超出作用域时自动销毁
return 0;
}
std::shared_ptr
std::shared_ptr
允许多个指针共享同一个资源。当所有指向资源的 shared_ptr
都被销毁时,资源才会被释放。
std::shared_ptr
示例:
#include <memory>
int main() {
std::shared_ptr<int> ptr1 = std::make_shared<int>(10);
std::shared_ptr<int> ptr2 = ptr1; // ptr1 和 ptr2 共享内存
cout << *ptr1 << endl; // 输出 10
cout << *ptr2 << endl; // 输出 10
return 0;
}
std::weak_ptr
std::weak_ptr
是用来解决循环引用问题的智能指针,它不会增加引用计数。
10. C++ 的 Lambda 表达式
Lambda 表达式是 C++11 引入的一种匿名函数对象,允许我们在代码中直接定义可调用的函数。
Lambda 表达式示例:
#include <iostream>
using namespace std;
int main() {
auto add = [](int a, int b) { return a + b; };
cout << add(3, 4) << endl; // 输出 7
return 0;
}
原文地址:https://blog.csdn.net/sjb2274540432/article/details/143944651
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!