c++基本语法
1|0(一)、基本概念
-
面向对象:其实质是从对象的角度出发
-
面向过程:时间维度
面向对象:事物维度
-
什么是面向对象?
- 我们就所有对象分为一个个的类
- 类是定义了对象的属性和操作
- 对象至少属于某一个类
- 类与类之间可以继承等关系
- 对象可以包含对象,告诉对象做什么(而不是how to do?)
-
计算机学科与自然学科的不同?
- 自然学科:先有对象,再有类
- 计算机学科:现有类,再有对象(人为的学科)
-
松耦合:可替换
-
封装(interface)
好处:
- 便于交流(交流外部)
- 保护内部(隐藏内部)
实例:将数据用private修饰,用getter,setter修饰
-
当一个类没有归到特定的
namespace
时,它将归属于一个default namespace
-
::
:解析符(可以看做是一个域的限制)如:
cat::say()
表示cat中的say函数 -
抽象(abstract)
- 从某一个层次去看
- 忽略细节
-
最近原则:本地变量的优先级高于全局变量
-
字段(field):类中的成员变量
-
c++复杂的体现
- 三种内存模型
- 堆栈(局部)
- 堆(动态)
- 全局变量空间
- 三种内存访问方式
- 直接
- 指针
- 引用
- 三种内存模型
-
所有oop语言都是默认动态绑定的,只有c++是静态绑定的:为了效率
-
相对关系的名词
- 声明(declaration)和定义(definition)
- 初始化(initialization)和赋值(assignment)
-
一个类下应该自己重写的函数
- 构造函数
- virtual析构函数
- 拷贝构造函数
-
全局变量初始化
- 对于同一个文件来说是有序的
- 对于不同文件来说是无序的
-
assert:如果一个指令是错误的,则退出
-
在计算机中set为置1,reset为置0
1|1(二)、编译过程
源文件
为了使中间文件变得简单,这里不用iostream头文件
-
main.cpp
-
TicketMachine.h
-
TicketMachine.cpp
尝试编译并生成中间文件
使用
g++ main.cpp -saves-temp
保存中间文件
-
预编译
对应文件:TicketMachine.ii
程序:预处理器
作用:将TicketMachine.h文件插入TicketMakchine.cpp文件
-
编译
对应文件:TicketMachine.o
软件:编译器
作用:将c++代码编译为更底层的汇编语句文件
注:
- 编译是以一个.cpp文件为单位进行编译的
-
汇编
对应文件:TicketMachine.o(二进制)
软件:汇编器
作用:翻译为最底层的机器码
-
链接
对应文件:main.exe(windows下)
软件:链接器(ld)
作用:将多个单元文件(.o)与库文件整合为可执行文件
注:对于头文件的处理(.h)尽在预编译阶段,但是与之对应的.cpp文件却可以是在链接阶段.换句话说,我们不是根据.h文件与对应的.cpp文件没有必然的联系
示例:
-
a.h
-
a.cpp
-
main.cpp
-
结果:10 10
-
结论:
- 尽管main.cpp中没有a.h,最后通过链接(
g++ -o test main.cpp a.cpp
)使用a.cpp中的f()
定义 - 单元a中尽管用的是a的头文件,最后使用的却是main中的f声明(输出结果为10 10而不是10 0),说明传入的默认参数是在编译时确定的,而不是运行时(具体说来,函数还是只需要两个参数,而这两个参数在编译时确定了。不是说多了两个函数,否则结果应该是0 10)
- 尽管main.cpp中没有a.h,最后通过链接(
-
1|2(三)、定义与声明
-
申明(declaration)与定义(definition)
-
c++所有声明都会添加一个下划线:如
global
经过编译后变成了_global
-
声明的几种方式
- extern变量
- 函数声明
- 类与结构体的声明
-
extern关键字:表明该变量在其它模块(相当于在当前模块留下了一个"洞")
-
TicketMachine.h
-
TicketMachine.cpp
为什么不能在.h文件放普遍变量定义(如
int temp=9
)?.h文件本身就是放声明的例外:
const int temp=9
,因为常量本身就是全局的 -
1|3(四)、oop三大特性
封装
继承
- 目的:实现软件重用(还包括其它方式,如对象组合)
多态(Polymorphism)
-
基本实现
- 上型
- 动态绑定:在运行时确定调用哪个函数(搭配virtual关键字使用)
-
c++有重载但没有覆写(即子类的函数会使父类的同名参数无效化) => c++可以通过virtual实现覆写
-
上型:父类属性是子类属性的子集,故子类对象可以作为父类对象调用
内存空间:父类属性在前,子类属性在后,子类与父类的公共函数分开存放
-
强制转换和造型
- 强制转换:改变了内存数据
- 造型:改变看待一个对象的角度(从子类的角度=>从父类的角度)
1|4(五)、命名空间
-
类全名:
std::ostream
即std命名空间下的类
2|0二、类与头文件
2|1(一)、基本格式
-
新建一个TicketMachine类
-
.h文件
-
.cpp文件
-
main文件
-
-
头文件形式
#incldue "xx.h"
:在当前目录找头文件#include <xx.h>
:在系统指定目录找头文件#include <xx>
:same as#include <xx.h>
-
#ifndf
与#define
#ifndf XXX
:如果没有XXX#define XXX
:那么定义XXX,知道后面的#endif
为止
尝试修改:
-
在.h文件添加
#define
-
.h文件
-
.cpp文件
-
生成.ii
-
注:可以看到没有类定义
-
-
在.cpp文件添加
#define
-
.h文件
-
.cpp文件
-
生成ii
-
注:可以看到没有类定义
-
-
总结:由于.cpp文件的预编译是自上而下读取的,那么相同头文件时,就不会再读取,避免了出现重复定义等错误(需要注意到.cpp文件间的相互引用可以是比较复杂的关系)
例:main.cpp引用了TicketMachine2.h和TicketMachine2.h,而TicketMachine2.h中实际也引用了TicketMachine.h
-
main.cpp
-
TicketMachine.cpp
-
TicketMachine2.cpp
结果:可以正常运行
对照(删去
#ifndef
):编译失败,且在main.ii文件下可以看到TicketMachine的重复引入main.ii
-
2|2(二)、类中的字段与函数
-
类中的字段也是声明的一部分(声明表示有这么个东西,定义表示这个东西在哪里)
-
字段是属于对象的,函数是属于类的
类的函数调用的参数就是类本身
-
main.cpp
-
TicketMachine.cpp
-
运行结果
深入理解:每个类的函数都有this这个本地变量,作为指针指向调用函数的对象
转换成c代码思考:
f(&ticketMachine)
-
-
this:类的成员变量,作为指针指向调用函数的对象
2|3(三)、构造与析构函数
-
构造函数的特点
- 构造函数的函数名就是类名
- 构造函数无返回值
-
对象的初始化
- 空间是在进入
{...}
后开始分配的 - 调用构造函数是在执行到相应语句
- 空间是在进入
-
default constructor与auto default constructor
- default constructor:没有参数的构造函数
- auto default constructor:有编译器自动生成的没有参数的构造函数
-
析构函数
格式:
~ClassName(){}
使用的两种情况:
- 被动释放:生命周期结束(以{}分隔)
- 主动释放:delete关键字
3|0三、动态内存分配
-
new
本质;运算符(返回值为地址)
作用:
- 分配内存
- 调用构造函数
-
delete
作用:
- 调用析构函数
- 收回内存
-
对应的堆中的内存分配
-
delete []className
:调用一组析构(与new className[]对应)-
TicketMachine.cpp
-
main.cpp
-
结果
逆序释放空间
-
4|0四、访问属性
-
常用修饰符
- private
- public
- protected:自身及子孙可以访问
-
private等修饰符实际上只在编译时刻起作用
-
main.cpp
-
TicketMachine.cpp
-
结论
- 在运行时,一个对象可以读取另一个同类对象的private属性(因为oop的访问修饰符特性只在编译时体现)
- 在运行时,一个对象不可读取另一个非同类对象的private属性(在编译时就无法通过)
-
-
friends(友元):友元函数可以访问类的private属性
-
TicketMachine.cpp
-
main.cpp
-
结论正确
-
-
class与struct的区别:
- class的属性默认修饰符为private
- struct的属性默认修饰符为public
测试:
-
TicketMachine.cpp
-
main.cpp
5|0五、初始化
5|1(一)、初始化参数列表
-
使用初始化参数列表
-
TicketMachine.cpp
-
Acount.cpp
-
main.cpp
-
结果
-
结论
- 参数化列表:初始化
- 构造函数:初始 => 赋值
-
-
任何类里面的成员变量都应该initialized里面做初始化,而不应该在构造函数中做初始化(否则需要default constructor)
使用构造函数初始化有时需要default constructor
6|0六、对象组合与继承
对象组合和继承都是实现软件重用的方式
6|1(一)、对象组合
- 两种使用方式
- 包含
- 引用(指针)
6|2(二)、继承
-
格式
-
protected:父类留给子类的接口用于访问private
-
子类与父类的构造与析构先后
- 父类先构造,子类后构造
- 子类先析构,父类后析构
代码:
运行结果:
7|0七、函数
7|1(一)、函数重载
-
c++的类中只有重载(overload)没有覆写(overwrite)
-
当子类有父类的同名函数,父类的同名参数(包括重载的)将全部被屏蔽
代码
7|2(二)、函数默认参数
-
默认参数值:只有右边的参数可以省略掉
格式:
-
TIcketMachine.h
-
TicketMachine.cpp
-
main.cpp
-
-
default argument是在编译时起作用,而不是运行时
-
a.h
-
a.cpp
-
main.cpp
-
结果:10 10
-
说明:
- 若默认参数是多了重载函数:则这些函数的第二个参数必然为0,因为main.cpp与a.cpp仅在链接步骤出现关联,而链接步骤只做了链接.
- 默认参数显然只能在编译时补上:由上显然只是一个函数然后适时补上了参数,都运行了还怎么补,那显然只能在编译时补(具体函数调用过程见下)
-
-
少用default value,以免阅读障碍等问题
7|3(三)、内联函数
-
作用:在编译时将对应的函数嵌入在源代码中(汇编代码阶段,以空间换时间)
-
优点
- 自身优点:避免了原函数需要的大量堆栈操作
- 相较于宏定义:编译器无法对宏定义做类型检查,但可以对内联函数做检查
-
缺点:当函数代码过长或者使用了递归,编译器不会使用inline
-
调用格式:与default arguments不同,内联函数inline只需要在.h文件
-
使用内联
-
main.cpp
-
a.h
-
观察 汇编文件main.s,共49行
-
-
不使用内联
-
main.cpp
-
a.h
-
a.cpp
-
观察汇编文件main.s,a.s,共27+24行(对于循环的调用应该效果会更明显吧)
-
-
-
类声明中的函数都是inline的,但通常更喜欢将需要inline的函数定义放在类外
8|0八、const关键字
-
extern const int bufsize
- 外部有一个变量bufsize,这个bufsize是不是const无所谓
- 在我这里不能修改
-
几种常见使用错误
-
const对应的变量为什么无法用来初始化数组
原因:函数的局部变量包括数组是存放在堆栈中的,在编译时由编译器分配对应的内存空间。当y是变量时,显然编译器无法分配数组需要的栈空间报错。
-
const变量的地址无法赋值给普通指针
原因:x认为不能修改,但p认为可以修改,两者地位平等,存在逻辑冲突
-
-
指针用法
char * const q = "abc"
:指针无法修改const char *p = "abc"
:无法通过改指针修改内容
注:const在*后,对象是const;const在*后,指针是const
-
变量存放的三个区域:
- 全局变量:全局数据区(其中常量在代码段中)
- 本地变量:堆栈(由编译器分配)
- 动态分配的内存:堆
-
const char *s = "Hello world!"
和char s[] = "Hello world!"
-
main.cpp
-
结果
-
结论
- 前者仅仅是将指针指向代码段(无法修改)
- 后者是在堆栈中分配了内存空间并赋值
-
-
函数参数为什么要传const指针
原因:
- 传入对象要在对栈中开辟一个临时的数据空间,并放入数据
- 传入指针可能会被修改,不安全\
结论:
- 传入const指针既能方便传入对象,又能保证不被修改
-
const对象:对象中的值都是无法修改的 => 在函数后加const保证对象中的函数不会修改成员变量(声明和定义后都要加const,实际上表示this要加const)
-
TIcketMachine.h
-
TicketMachine.cpp
-
main.cpp
-
-
const构成重载
9|0九、引用
-
基本定义:引用可以看做是别名
本质:用
const *
实现 -
与const
-
与*
int *& p;
:一个int *
类型的引用int &* p
:非法
-
使用:函数参数
-
无法做函数重载
-
没有reference的reference
10|0十、virtual关键字
-
使用virtual关键字的类第一个int字节不再是普通的属性,而是指针,指向vtable(虚拟函数表)
-
main.cpp
-
结果
-
结论:在使用virtual情况下,同一个类的首地址指向vtable(虚拟函数表)
-
-
子类与父类的vtable位置不同
-
main.cpp
-
结果
-
结论:父子类的vtable地址不同(哪怕父类没有覆写过虚函数)
-
-
类的赋值与指针的赋值
-
main.cpp
-
结果
-
结论:不言自明
-
-
如果一个类里面有一个析构函数,那么析构函数也应该是virtual的
-
main.c
-
结果
-
结论:由于普通函数的静态链接导致这里调用的A类的析构函数(而不是本身的B类)
-
main.c修改为virtual函数
-
结果
-
结论:成功自下而上调用析构函数
-
-
通过
FatherClass::func()
调用父类函数-
main.c
-
结果
-
结论:子类用父类民调用父类函数
-
-
通过virtual实现override
-
main.cpp
-
结果
-
11|0十一、拷贝构造
-
特点
- 没有调用普通构造函数(与普通的new比较)
- 发生的不是字节到字节的拷贝,而是member到member的拷贝
-
基本格式:
T::T(const T&); //由c++给出,可以自己覆盖
-
函数参数
-
main.cpp
-
结果
-
结论:f中的参数和返回值所使用的都没有走构造函数,而是通过其它方式(拷贝构造)
-
-
基本使用
-
main.cpp
-
结果
-
结论:
A a = 1; //等效与A a(2)
-
-
覆写
-
main.cpp
-
结果
-
问题:f(a)是一个拷贝构造,那么A b=f(a)为什么也只有一个拷贝构造?
-
main.cpp
-
结果
-
结论:根据翁凯老师的讲解,这里编译器做了个优化(即要不要return回一个临时对象)
-
原形式
-
编译器优化后的形式
-
main.cpp验证
-
结果
-
结论:没有使用拷贝构造(被优化了)
-
-
-
调用拷贝构造的几种情况
- 对象作为函数的参数传入
- 用一个对象初始化另一个对象
-
拷贝构造对指针属性的影响:将指针指向了同一个位置 => 然而许多时候我们并不希望这样
- 注意到c++中的string有拷贝构造,而c中的
char *
没有,因此更推荐用string - 一般情况下建议自己写拷贝构造
- 注意到c++中的string有拷贝构造,而c中的
-
禁止拷贝构造:将拷贝构造函数private
-
拷贝构造与初始化的不同
- 拷贝构造:调用拷贝构造函数
- 初始化:调用赋值运算符
12|0十二、static关键字
-
static在c中的使用
- 本地变量(hidden):将变量(函数)限制在当前文件使用
- 持久化存储(persistent):而不是在某个函数调用后就收回
-
static在c++中的使用
- 修饰类:存储是全局的(链接时分配),但初始化依然是在函数内部,只初始化一次
- 修饰属性:属于类的静态成员属性和静态成员函数(这里表现的是其persistent特性,他的hidden特性由private替代了)
-
类中的static属性的初始化
-
需要在.cpp中初始化
-
main.cpp
-
结果
-
-
不能通过初始化参数列表初始化
-
在内部通过this访问
-
在外部通过
ClassName::field
访问
-
-
静态成员函数调用静态参数
13|0十三、运算符重载
-
限制
- 只能对已有运算符重载
- 重载必须对一个类或枚举类型
- 重载必须保持原来的参数个数与优先级
-
通过operator关键字重载
-
运算符重载类型
-
全局函数:完整写出所有参数
-
成员函数:使用reciver作为实际调用运算符重载函数的对象(在全局的重载函数中是第一个参数)
以
a + b
为例,实际可以写作a.operator+(b)
,故reciver是左边的a应该选择作为成员函数的情况:
- 单目运算符
- 常见的如:
=,(),->,->*
-
-
基本概念
-
return value
- 修改当前对象
- 返回新对象
- 新对象可以被修改
- 新对象不能被修改
-
常见操作符原型
-
+-*/%^&|~
-
! && || < <= == >= >
-
[]
-
++ --
注:通过int,编译器能够识别那个是
prefix
哪个是postfix
-
=
-
强制转换
-
通过构造函数转换
-
隐式转换
main.cpp
-
显示转换
main.cpp
-
-
使用强制转换(少用)
-
-
-
14|0十四、模板
-
场景:不同类型的数据需要用到相同的代码段
选择:
- 使用基类
- 代码克隆(不好管理)
- 无类型列表(void *)
-
函数模板
-
main.cpp
-
结果
-
本质
- 读取模板函数声明(有些在.h文件)
- 确定所需要的模板函数参数类型
- 生成函数
-
-
类模板:类模板里面的每个函数都是函数模板
使用:
-
main.cpp
-
TicketMachine.h
-
结果
注:
- 模板类中函数的声明与定义都应该写在.h文件,否则链接错误(从编译角度可以理解,因为倘若放在另一个模块那是无法直接调用,必须将对应的函数定义放在同一个模块来真正构造函数)
- 类模板可以有默认参数
- 类模板可以继承
-
15|0十五、异常
-
场景:不可控因素,例如读取一个文件,而那个文件实际不存在
-
优点:将业务逻辑和错误处理逻辑分开
-
使用
-
抛出异常,中断并上溯:
throw VectorIndexError(index);
注:
-
这个对象不是new出来的,故不在堆栈,而是在堆里
-
throw;
会直接将catch到的异常继续上抛 -
new关键字无法申请到内存,则抛出
bad_alloc异常
-
-
捕获异常
注:
-
参数中的
...
表示捕获所有异常 -
匹配顺序(自上而下)
- exact
- base
- ellipse(...)
-
通过在函数定义后添加操作表示最多能抛出的异常
-
异常传递:建议用引用
-
构造函数丢异常也可能造成内存泄漏
-
-
16|0十六、流
-
特点:一维单方向
- 缺点
- 比较慢
- 比较繁琐(内部)
- 优点
- 开发效率较高
注:与之相对的fscanf等接口是随机访问的
- 缺点
-
二进制文件:不以人的阅读为目的的文件(因为从某个角度来说,所有的文件都是二进制的)
-
c++中的流对象
- cin
- cout
- cerr:错误
- clog:日志
-
操纵器(manipulate):操纵器,例如:endl,flush
17|0十七、STL模板
-
主要内容
-
数据结构
常用:
- Vector
- Deque:expands at both end
- List:double-linked
- sets and maps
特点:
- 自动增长
- 建议用
!=.end()
判断是不是走到头了
-
算法(函数模板)
常用:
- sort
- search:二分
-
-
通用的iterator
-
运算符通常是有结果的
-
可以对对象进行赋值(虽然看起来毫无意义)
-
尽量将代码建筑在已有的代码基础上
__EOF__
本文链接:https://www.cnblogs.com/Arno-vc/p/15988122.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix