C++学习笔记
从C到C++
const
修饰符
- C语言const限定符含义为一个不能改变的变量,本质还是变量,C++的const限定符的含义为一个有类型描述的常量,必须初始化
- 在C++中,被const修饰的变量都变成不可修改的左值(常量)
- 常量必须被初始化
- 被修饰为常量之后不能被赋值.
- 常量指针指的是可以通过该指针取值,但不能通过该指针赋值.
int nNum = 100;
// pNum是一个指针常量
const int* pNum = &nNum;
nNum = 0 ; // 被指向的变量不会受到影响
*pNum = 0; // 错误, 通过常量指针不能赋值.
int b = *pNum; // 正确, 可以取值.
int c = 10;
pNum = &c; //正确, 常量指针可以被重新赋值,指向其它内存
- 指针常量指的是可以通过该指针取值和赋值,但不能修改指针的指向.
- 常量指针不能赋值到非常量指针
简述const int *p、int * const p、int const *p三种定义方式的作用和区别。
//const在 * 号左边,常量指针,指针可修改,指针指向的内存空间不可修改
int a=5;
int b=5;
const int *p1=&a; //指针p1指向const int类型
int const *p2=&a;
p1=&b; //指针可修改
*p1=5; //指针指向的内存空间不可修改,即*p不能被修改
//const在 * 号右边,指针常量,指针不能修改,指针指向的内存空间可修改,即(*p可修改)
int * const p; //错误,指针常量必须初始化
int * const p =&a; //指针常量
p=&b; //错误,指针不能修改
*p=5; //指针指向的内存空间可修改,即(*p可修改)
内联函数
在函数定义前加inline
关键字
堆内存申请和释放
C语言中申请堆空间的是malloc
和free
函数,C++中申请堆空间用的是new
和delete
运算符
new
和delete
new[]
和delete[]
int* p1= (int*)malloc(10 * sizeof(int));
free( p1 );
// 相同功能的C++版本:
int* p2 = new int[10];
delete[] p2;
// 只申请一个元素:
int* p3 = new int;
// 只申请一个元素,并初始化为0
int* p4 = new int(0);
malloc
和free
函数和new
和delete
运算符的区别:
malloc
需要自己计算字节数,new
会根据类型自动计算字节数malloc
返回一个空指针,需要自己进行类型转换,new
会自动匹配类型malloc
不会自动调用类的构造函数,new
会自动调用类的构造函数free
一个对象时,不会自动调用类的析构函数,delete
可以,若释放数组需要加括号delete[]
malloc
和free
是函数,new
和delete
是运算符
函数重载
函数重载又称为函数的多态性,构成函数重载的条件:
- 相同的作用域
- 参数不同
- 形参数量不同
- 形参类型不同
- 形参的顺序不同
注意: 跟返回值类型没有关系
int sum(int n1 , int n2){return n1+n2;}
double sum(double d1,double d2){return d1+d2;}
int main() {
sum(1,2);// 调用第一个
sum(1.1,2.2);// 调用第二个
sum(1,1.2);// 调用第三个.找到不到更适合的时候,会将参数隐式转换再调用.
}
函数重载的本质:
此代码在C语言中会因为函数重名而编译失败,在C++中则是一个典型的重载函数。这是因为在C++中引入了名称粉碎机制
,它们的名称可能长这样
?sum@YHSLD@Z
?sum@YSKFLD@Z
?sum@YHPSFD@Z
根据参数的不同,把函数名字修改成不同的名字
默认参数
默认参数就是在函数声明和定义时可以给形参赋一些默认的参数,调用函数时,若没有给出实参,则按指定的默认参数进行计算。
注意事项:
- 函数没有声明时,在函数定义中指定形参的默认值
- 函数既有声明又有定义,声明时指定默认参数后,定义时不能再指定
- 当使用了默认参数的同时还使用了重载容易造成二义性
int sum(int n1,int n2, int n3=0, int n4=0)
{ return n1+n2+n3+n4; }
// 形参默认值和函数重载可能会产生冲突
int sum(int n1,int n2){ return n1+n2;}
int main() {
sum(1,2); //产生二义性
sum(1,2,3);
sum(1,2,3,4);
}
引用
- 定义格式: 类型 &引用名 = 变量名
- 注意事项:
- 引用就是变量的一个别名
- 定义引用一定要初始化,指明该引用变量是谁的别名
- 引用一经初始化就不能再引用其他对象
引用传参:
一般格式:
int swap(int &nNumA, int &nNumB);
引用和指针的区别:
- 引用访问变量是直接访问,指针需要保存变量的地址,是间接访问
- 引用是变量别名,本身不单独分配内存空间,指针有自己的内存空间
- 引用一经初始化不能再引用其他变量,指针可以
- 尽可能使用引用。
三种传参方式比较:
- 值传递:实参要初始化形参要分配空间,将实参内容拷贝到形参
- 指针传递:传递的是地址,能够间接修改函数外部的变量,其本质仍是值传递
- 引用传递; 实参初始化形参时不分配空间,而是形参实参融为一体,修改形参就修改的实参
作用域符号
C++引入了一个新的符号,由两个冒号组成:::
,被称为作用域符号,用来解决变量函数重名问题。
C++输入输出机制
基本输入和输出:
使用 cout 输出,使用 cin 进行输入, 这两个是全局对象. 能够自动解析要输入或输出的数据类型.
- 使用cout和cin时需要包含头文件和std命名空间
#include <iostream>
using namespace std;
- 使用setw ,setfill需要头文件
格式化输入输出:
控制符 | 描述 | 备注 |
---|---|---|
dec | 按十进制输出 | |
hex | 按16进制输出 | |
oct | 按八进制 | |
setfill© | 设填充字符为c | 可为setw设置填充字符 |
setw(n) | 设域宽为n个字符 | 值的位数大于n,按原宽度输出,默认用空格填充 |
setprecision(n) | 设置小数精度为n位 | |
setiosflags(ios::fixed) | 小数方式表示 | |
setiosflags(ios::scientific) | 指数表示 | |
setiosflags(ios::left) | 左对齐 | |
setiosflags(ios::right) | 右对齐 | 默认右对齐 |
命名空间
- 定义命名空间的方式: 使用 namespace 关键字
- 使用命名空间内部的符号(变量,函数,类型名)
- 直接使用 using namespace 命名空间名;
- 优点是: 将整个命名空间的符号到导入进来,使用这些符号无需加作用域说明
- 缺点: 容易造成重名冲突.
- 使用声明式: using std::cout , 将某个命名空间的符号导入进来.(推荐的做法)
- 优点 : 只导入一个符号, 其它的不导入,这样可以降低命名重名的冲突几率.
- 使用作用域说明, 在使用的时候,在符号名前加上作用域选择符. 指定符号的作用域(推荐的做法)
- 直接使用 using namespace 命名空间名;
面向对象的程序设计
面向对象程序设计OOP(Object-Oriented-Programming)的三要素:对象、类、和继承
- 对象
- 把一系列数据和处理数据的过程(操作和方法)当成一个整体,这个封装体就是对象
- 类
- 类是创建对象的模板,它包含这所创建对象的状态描述和方法定义,对象是由特定的类创造的,某个类所创建的对象也称为这个类的实例
- 一个类的所有对象都有相同的数据结构,并且共享相同的实现操作的代码,二各个对象又有具体的不同的状态(主要表现为内部数据不同)
- 继承
面向对象的三大特性:封装、继承、多态
- 封装性是基础,继承性是关键,多态性是补充
类
面向对象的三大特征: 封装,继承,多态
封装 - 指的是将一些操作或数据隐藏起来使得类的外部无法使用这些操作或数据(成员变量,成员函数).
类的组成
- 成员变量
- 一般成员变量以 m_ 作为前缀.
- 一般成员变量都是设置为私有(如果类外要修改或者是获取该成员的值, 则设置两个公有成员函数来读写,一般这两个公有的成员函数称之为 getter , setter
- 成员函数
- 一般成员的功能必须和类所保存的数据有关. 如果无关,就不应该将函数作为该类的成员函数.
- 可以在类内定义, 也可以在类外定义
- 可以将声明和定义分别定义在头文件和原文件中. 使用这个类时,只需包含头文件即可.
- 访问控制符( 类的默认访问控制符是私有 )
控制了两种对象的访问权限: 1. 类的派生类(子类) 2. 类域的外部.- 私有 : private - 只能在类内使用.
- 保护: protected - 只能在类内和子类内使用.
- 公共: public - 在类内,子类内,类外都能使用.
- this 指针
- 在通过类对象调用成员函数时,C++会自动把对象的首地址传递到成员函数内部, 并使用 this 关键字来代替这个对象的首地址.
类的声明、定义和应用
类的定义格式:
- 类定义包含两个部分
- A、说明部分(做什么)
数据成员(名称、类型)
成员函数(方法) - B、实现部分(怎么做)
成员函数定义和实现
- A、说明部分(做什么)
- 类定义的一般形式
//声明部分
class<类名>
{
public:
公有数据及成员函数
protected:
保护数据及成员函数
private:
私有数据及成员函数
};
//实现部分
各成员函数的实现
- A、公有成员提供类的接口功能,不仅可以被成员函数访问,而且可以在程序中被访问
- B、私有成员是被类隐藏的数据,外部无法访问,派生类也无法访问,通常将数据成员定义为私有成员
- C、保护成员外部无法访问,派生类函数可以访问,
定义对象
- 格式:<类名> <对象名表>;
- 定义 Location类的对象如下:
- Location objA,objB,objC[10], *pobjD;
- objA objB 为一般对象
- objC[10] 对象数组
- pobjD 是指向类Location对象的指针
定义成员函数
- 定义类中成员函数可以用以下三种方式:
- A 成员函数的定义和实现在类体中完成
- B 成员函数的定义和实现在类外中完成
- C 成员函数的定义和实现与类体在不同文件中完成
- 在类体外定义成员函数时必须按照以下格式:
<函数类型> <类名>::<成员函数名>(参数表)
{
<函数体>
}
调用成员函数
- 一个对象要表现其行为,就要调用它的成员函数。
- 用成员访问符调用
- 用this指针
- 注意:
- 一个类对象所占据的内存空间是由它的数据成员所占据的内存空间综合决定
- 类的成员函数不占据对象的内存空间
再论程序结构
- 类的作用域简称类域,范围是指在类所定义的类体中。
- 对象的生存期是指对象从创建开始到释放为止的存在时间,可分为三类:
- 局部对象:定义在一个函数体内或程序块内,作用域和生命周期都是局部的
- 全局对象:定义在某个文件中,作用域为包含该文件的整个程序,生命期是全局的
- 静态对象:分为内部静态对象和外部静态对象,生命期都是全局的,前者作用域为定义它的函数体和程序块,后者作用域定义为它的文件
- 类名允许与其他变量名或函数名同名,可通过下面方法实现正确的访问
- A 如果一个非类型名隐藏了类型名,则类型名通过加前缀class访问
- B 如果一个类型名隐藏了非类型名,则用一般作用域规则访问
类和结构体的区别
- 默认访问区别:
- 类定义中默认情况下成员访问级别是private
- 结构体定义中默认情况下的成员访问级别是public
类对象的初始化问题
struct MyStruct {
int n1;
int n2;
char name[32];
};
class MyClass {
int n1;
public:
int n2;
char name[32];
};
int main() {
MyStruct obj1 = { 1,2,"3" };
// 初始化错误:
// 1. 类内的成员是非公有的,
// 在类外无法访问,也就无法被初始化
// MyClass obj2 = { 1 };
}
总结 : 只有类中的成员包含了私有,那么就无法在类外初始化.
C++提供的解决方法: 使用构造函数来初始化.
构造函数
- 是一个特殊的函数:
- 函数没有返回值类型
- 函数名和类名一样.
- 是被编译器自动调用的.
- 构造函数的调用时机: 定义一个对象时就被调用, 目的在于:初始化这个对象.
- 构造函数有初始化列表, 专门用于初始化成员变量
- 构造函数的调用顺序
- 如果类内有成员是类类型, 那么先调用这些成员的构造函数, 最后再调用类自身的构造函数.
- 构造函数可以被重载
- 一个类如果没有定义构造函数和析构函数,C++编译器会自动提供一个无参的默认构造函数
- 如果一个类中定义了构造函数(无论任何形式的构造函数),编译器都不会再提供无参的默认构造函数
析构函数
是一个特殊的函数:
- 没有返回值类型
- 函数名和由 ~ 和类名组成的
- 是被编译器自动调用的.
- 调用时机: 当类对象的内存空间被销毁时.
- 如果类中有成员变量是类类型, 则它们析构函数的调用顺序正好和构造函数调用顺序相反.
构造函数和析构函数的作用
- 构造函数是负责初始化成员变量, 或者申请资源(堆空间, 文件)
- 析构函数主要是负责清理资源(释放堆空间, 关闭文件)
成员变量的初始化方法
- 可以在构造函数的初始化列表中进行初始化
- 对象成员的构造顺序按照在类中的定义顺序来决定,跟初始化列表中的顺序无关
- 可以直接在成员变量的定义语句中给出初始值(需要c++11以上的标准的语法)
- 如果成员变量是类对象的时候, 这个类对象所属的类没有默认构造函数, 那么就需要在初始化列表中,通过对象名来主动调用该类的其它形式的构造函数.
- 有参构造函数初始化列表
- const类型的成员,只能在初始化列表中初始化
- 引用类型的成员,只能在初始化列表中初始化
- 有参构造的对象成员(对象所对应的类没有默认构造函数),只能在初始化列表中初始化
不同重载形式的构造函数
按照构造函数的参数类型,个数对其分类:
参数类型 | 参数个数 | 术语 |
---|---|---|
无 | 0 | 默认构造函数 |
非本类类型 | 1 | 转换构造函数 |
本类类型的引用 | 1 | 拷贝构造函数 |
不限 | 2个以上 | 带参构造函数 |
- 可以在构造函数前加 explicit 关键来禁用隐式转换.
拷贝构造函数
- 在C++中,提供了用一个对象值创建并初始化另一个对象的方法,完成该功能的是拷贝构造函数
- 什么情况会调用拷贝构造函数
- 把对象作为实参进行函数调用时,系统自动调用拷贝构造函数实现把对象值传递给形参对象.
- 当函数的返回值为对象时,系统自动调用拷贝构造函数对返回对象值创建一个临时对象,然后在将临时对象值赋给接收函数返回值的对象
- 浅拷贝
- 多个对象指针指向一块堆空间,析构时会多次释放这块堆空间,会造成程序崩溃.
- 如果成员变量有一个时指针变量,就得写深拷贝函数
类的默认函数
定义一个类会默认生成几个函数:默认构造函数、默认拷贝构造函数、默认析构函数、默认赋值运算符 这四个是我们通常大都知道的。但是除了这四个,还有两个,那就是取址运算符和 取址运算符 const即总共有六个函数。
class Empty
{
public:
Empty(); // 缺省构造函数
Empty( const Empty& ); // 拷贝构造函数
~Empty(); // 析构函数
Empty& operator=( const Empty& ); // 赋值运算符
Empty* operator&(); // 取址运算符
const Empty* operator&() const; // 取址运算符 const
};
但是,C++默认生成的函数,只有在被需要的时候,才会产生。即当我们定义一个类,而不创建类的对象时,就不会创建类的构造函数、析构函数等。
类的派生与继承
C++中的代码重用方式就是继承。继承的优势:
- 继承是使用已经编写好的类来创建新类,新的类具有原来类的所有属性和操作,也可以在原基础上作一些修改和增加
- 新类称为派生类,原有类称为父类或基类
- 派生类是基类的具有化,一般来说派生类比基类的表示范围要小
继承的语法:
class MyClass{
int m_nNum;
public:
void fun( ){cout <<"MyClass::fun\n";}
};
// 没有指明继承方式的时候, 默认是私有继承.
// 继承的语法: `: MyClass`
// 称MyClass2继承了MyClass
// MyClass2是MyClass的子类.
class MyClass2 : MyClass {
};
// 在C++中,结构体实际和类一样.
// 只是默认访问属性和默认继承方式有区别:
// 结构体的默认访问属性和默认继承方式都是公有的
// 而类则都是私有的.
struct MyStruct{};
struct MyStruct2 : public MyStruct{
};
继承的方式:指的是在继承时指定的三种方式: public , protected , private
继承对子类造成的影响
- 公有继承时: 父类成员的访问属性在子类中保持不变.
- 保护继承时: 父类成员的私有属性和保护属性保持不变, 父类中的公有属性成员在子类中变成保护属性.
- 私有继承时: 父类中所有属性的成员在子类中都变成私有属性.
- 父类中原本就是私有的属性, 在子类中不可访问.
总结一下,其实就是:
- 内部访问:派生类成员函数访问基类
- 可以访问基类公有和保护成员
- 不可访问私有成员
- 外部访问:派生类对象访问基类
- 公有继承时可访问基类公有成员
- 其它均不可访问
通过代码验证:
#include "pch.h"
#include <iostream>
class A {
public: int m_public = 0x11111111;
protected: int m_protected = 0x22222222;
private: int m_private = 0x33333333;
};
// 2. 验证三种继承方式对子类访问父类成员的影响
class B1 : public A {
public:
void fun() {
m_public = 0;
m_protected = 0;
// 2.1 公有继承时, 父类的保护成员和公有成员都能访问
// 唯独私有成员无法访问.
//m_private = 0;
}
};
class B2 : protected A {
void fun() {
m_public = 0;
m_protected = 0;
// 2.2 保护继承时, 父类的保护成员和公有成员都能访问
// 唯独私有成员无法访问.
//m_private = 0;
}
};
class B3 : private A {
int m_b3 = 0x44444444;
void fun() {
m_public = 0;
m_protected = 0;
// 2.3 私有继承时, 父类的保护成员和公有成员都能访问
// 唯独私有成员无法访问.
//m_private = 0;
}
};
// 2.4 总结 : 验证三种继承方式对子类访问父类成员的影响
// 2.4.1 无论何种继承方式, 在子类中都无法访问父类的私有成员
// 其它属性的成员都可访问.
// 3. 验证三种继承方式对孙子类访问父类成员,爷爷类成员的影响
class C1 : public B1 {
public:
void fun1() {
m_public = 0;
m_protected = 0;
// 父类和孙子都以公有方式继承
// 最终是私有成员无法访问.
//m_private = 0;
}
};
class C2 : protected B2 {
public:
void fun1() {
m_public = 0;
m_protected = 0;
// 父类和孙子都以保护方式继承
// 最终是私有成员无法访问.
//m_private = 0;
}
};
class C3 : private B3 {
int m_c3 = 0x55555555;
public:
void fun1() {
// 如果父类以私有方式继承了
// 爷爷类, 爷爷类的所有成员
// 在父类中都是私有的
// 那么孙子类都无法访问这些成员.
//m_public = 0;
//m_protected = 0;
// 父类和孙子都以公有方式继承
// 最终是私有成员无法访问.
//m_private = 0;
}
};
int main()
{
B1 obj1;
B2 obj2;
B3 obj3;
// 1. 验证继承方式对类外的影响
// 1.1 公有继承, 在类外可以访问到父类的公有成员
obj1.m_public = 0;
// 1.2 保护继承和私有继承,在类外无法访问到父类的公有成员
// 原因:
// 保护继承时, 父类的公有成员在子类中变成保护成员
// 私有继承时, 父类的公有成员在子类中变成私有成员
// obj2.m_public = 0;
// obj3.m_public = 0;
C3 obj;
std::cout << sizeof(C3);
}
继承对子类对象内存大小的影响
继承之后, 无论何种继承方式, 无论是否能被访问到, 子类对象的大小等于 子类成员自身的大小 + 父类成员的大小.
子类对象中, 成员变量的内存布局:
- 先排列父类的成员
- 再排列子类的成员
这只是最简单的,多继承,虚继承,继承方式不一样,内存布局会有变化。
继承对构造函数,析构函数调用顺序的影响
构造函数的调用顺序:
- 基类构造函数
- 数据成员(其他类对象作为数据成员时)的构造函数
- 派生类(自己)构造函数
析构函数的调用顺序: - 派生类(自己)析构函数
- 数据成员(其他类对象作为数据成员时)的析构函数
- 基类析构函数
在子类中调用父类的成员函数
- 访问权限允许的情况下,直接通过函数名调用
父类和子类存在同名函数
父类和子类的函数重名现象称为重定义。
调用原则:就近原则(在哪个类中调用,被调用的同名函数就是该类的)
如何想要调用父类的同名函数,在函数名前面加上作用域选择符就行了。
菱形继承造成的影响
- 菱形继承: 由于两个父类重复继承了同一个爷爷类. 因此, 在孙子类中, 就有了两份相同的爷爷类的成员。
- 如果要将爷爷类的重复成员去重, 可以使用虚继承 ,语法: 在父类继承爷爷类时, 使用 virtual 关键字。
- 此时, 爷爷类就变成了虚基类. 虚基类在孙子类中的重复成员就会被去掉重复部分, 只保留一份。