c++对于c语言的加强
namespace 命名空间
没啥可以讲的,避免了类名字,方法,成员变量的冲突。
实用性增强
变量可以边定义边用。
变量检测增强
int a;
int a=8;会报错了,不像c 不会报错。
struct 类型增强
c++的struct 类似于class ,唯一的不同是class定义的函数默认是private 的,但是struct 定义的函数是public 的。而且,c不认为这是一种新类型,仍然需要struct AAA,进行定义,但是c++则不需要,直接使用AAA 进行定义对象。即认为是一种新的类型。
C++中所有变量和函数都必须有类型
如下代码在c语言是OK的,但是在c++就不行咯。
f(i) {
printf("i = %d\n", i); }
g() {
return 5; }
新增bool 类型,返回值为true 和false
三目运算符增强
返回的是引用,而不是值,所以可以对其进行赋值。
const 增强。
可以分清输入与输出的参数。带const往往是输入。在c语言中,const 是冒牌货。
c++的const由编译器处理,提供类型检查与作用于检查。
#define由预处理器处理,单纯的文本替换。
c++的const 可能分配空间,也可能不分配空间。
当const 常量是全局的,并且需要在其他文件使用。会分配空间。
当使用&操作符。取const 地址,会分配空间。
const int &a=10; const 修饰引用,也会分配空间。
引用。
Type& name = var;
引用的规则。
1.引用没有定义,是一种关系声明,声明和他原有的某一个变量关系,故不分配 内存,与被引用变量有着同样的指向地址。
2.声明的时候,必须得初始化,一经声明,不能改变。
引用的本质
#include <iostream>
int main() {
int a = 10;
int &b = a; // 注意: 单独定义的引⽤用时,必须初始化。
b = 11;
printf("a:%d\n", a);
printf("b:%d\n", b);
printf("&a:%p\n", &a);
printf("&b:%p\n", &b);
return 0; }
引用在c++中的内部实现是一个常指针,即一个指向不能改变的指针。
Type& name <===> Type* const name
c++在编译过程中。使用常指针作为内部实现。因此,引用所占用的空间与指针大小相同。
从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这个是c++为了实用性,而做出的细节隐藏。
void func(int &a)
{
a = 5; }
void func(int *const a)
{
*a = 5; }
间接赋值的3各必要条件
1定义两个变量 (一个实参一个形参)
2建立关联 实参取地址传给形参
3*p形参去间接的修改实参的值.
引用的实现上。只不过是把,间接赋值成立的三个条件的后面2个合二为一了。当实参传给形参引用的时候,只不过是c++编译器帮我们程序员手工。取了一个实参的地址,传给了一个常引用。(常量指针)
引用作为函数的返回值(引用当左值)
当函数返回值为引用时候。若返回栈变量,不能成为其他引用的初始值。(不能作为左值引用。)
当函数返回值为引用时,如果返回静态变量或全局变量,可以成为其他引用的初始值。可以作为右值,也可以成为左值。
什么是引用当左值?
int& fun(){
int a=10;
return a;
}
fun()=10;
//如下的只是值拷贝。
int x=fun();
指针引用
const 引用
const对象的引用必须是const的,将普通引用绑定到const 对象是不合法的。如下。
const int a=1;
int &b=a;
//常引用在初始化其实变成了2个步骤
int temp = val;
const int &ref = temp;
这也是为什么
int &a=1;//是错误的
const int &a=1;//却是正确的的原因。
结论:
1.const int &a;相当于const int * const a;
2.普通引用相当于int * const e;
3.当使用const对应用初始化的时候,c++会为常量值分配空间,并将引用名作为这段空间的别名。
4.使用字面量对const引用初始化后,将生成一个只读的变量。
inline 函数
可以想是宏函数的拓展,内联的特点。
1.没有函数调用的开销。(压栈,跳转,返回)
2.内联函数由编译器处理,直接将编译后的代码插入调用的地方。宏片段由预处理器处理,进行简单的文本替换,没有任何的编译过程。
同时存在着一定的限制。
- 不能存在任何形式的循环语句
- 不能存在过多的条件判断语句
- 函数体不能过于庞大
- 不能对函数进行取址操作
- 函数内联声明必须在调用语句之前
当函数体执行的开销远远大与压栈,跳转,返回的开销,内联将失去意义
函数重载
C++利用 name mangling(倾轧)技术,来改名函数名,区分参数不同的同 名函数。
实现原理:用 v c i f l d 表示 void char int float long double 及其引 用。
每一个方法都有一个特殊的Symbol 符号。
拷贝构造函数调用的场景
场景1
Test t1(10);
Test t2=t1;
场景2
Test t1(10);
Test t2(t1);//使用对象t1初始化对象t2
1.函数返回值是一个元素(复杂类型),返回的是一个新的匿名对象(所以会调用匿名对象的copy构造函数)
2.有关匿名对象的去和留
如果匿名对象去初始化另外一个同类型的对象,匿名对象被扶正
如果用匿名对象去赋值给另外一个同类型的对象,匿名对象被析构
new delete
注意,new 与delete 返回的都是指针类型。是一块内存地址。不能够用普通对象直接接收,与malloc delete 有着同样的功能,唯一的不同是,malloc free在初始化类对象的时候,是不会自动的帮我们调用构造函数,但是new 与delete 是会自动的帮我们调用。
静态成员的定义
static 数据类型 成员变量; //在类的内部
//初始化
数据类型 类名::静态数据成员 = 初值; //在类的外部
//调⽤用 类名::静态数据成员 类对象.静态数据成员
编译器对属性和方法的处理机制。
普通成员变量,存储于对象中,与struct变量有着相同的内存布局和字节对齐方式。
静态成员变量,存储于全局数据区
成员函数,存储于代码区
很多对象共用一块代码,代码如何区分具体对象是那?
如下的图应该算是很经典的一张图了。
c++的普通成员函数都隐含一个包含当前对象的this指针
静态成员函数不包含指向具体对象的指针。普通成员函数含有一个指向具体对象的指针。
友元
友元声明以关键字friend开始,只能在类定义中出现,因为不是授权类的成员,所以他不受类声明的public private 和protected的影响
利弊:可以直接访问类中的私有成员,破坏了类的封装性和隐蔽性。但是提高了编码的灵活性。
注意事项
友元关系无法继承
友元关系无法交换
友元关系不具有传递性
运算符重载
执行重载的时候,记得对参数加const
双目运算符重载
//使⽤用: L#R operator#(L,R);
//全局函数 L.operator#(R); //成员函数
Complex& operator+=(const Complex &c) {
this->_x += c._x; this->_y += c._y;
return * this;
}
重载规则请务必记住
//重载+号 (全局)
friend const Complex operator+(const Complex &c1,const Complex &c2);
//重载+号(成员)
const Complex operator+(const Complex &another);
//a+=b;对于a应该返回的还是原来的a 所以不能为const
Complex& operator+=(const Complex &c)
//a-=b;同样的是返回可以修改的内容。
friend Complex& operator-=(Complex &c1, const Complex & c2);
//前置++ 注意返回的是引用
friend Complex & operator++(Complex& c);
friend const Complex operator++(Complex &c,int);
friend ostream & operator<<(ostream &os, const Complex & c);
friend istream & operator>>(istream &is, Complex &c);
//赋值运算符重载 返回的是引用,不能用const 修饰,其目的是连等式
A& operator=(const A& another)
//数组符号的重载
类型 类 :: operator[] ( 类型 ) ;
结论:
1,一个操作符的左右操作数不一定是相同类型的对象,这就涉及到将该操作符函 数定义为谁的友元,谁的成员问题。
2,一个操作符函数,被声明为哪个类的成员,取决于该函数的调用对象(通常是左 操作数)。
3,一个操作符函数,被声明为哪个类的友员,取决于该函数的参数对象(通常是右 操作数)。
重载&& 和||会丢失短路特性。
无参构造函数调用不能带括号!
类的继承方法
class 派⽣生类名:[继承⽅方式] 基类名 { 派⽣生类成员声明;
};
private成员在子类中依然存在,但是却无法访问到。不论何种方式继承 基类,派生类都不能直接使用基类的私有成员 。
三种继承关系
保护继承中,基类的公有成员和私有成员都以保护成员的身份出现在派生
类 中,而基类的私有成员不可访问。
当类的继承方式为私有继承时,基类中的公有成员和保护成员都以私有成
员身 份出现在派生类中,而基类的私有成员在派生类中不可访问。
当类的继承方式为公有继承时,基类的公有和保护成员的访问属性在派生
类中 不变,而基类的私有成员不可访问。
不论是什么类型的继承,在基类中不能使用的,只有私有成员,其余就算因为私有继承而退化为私有的,子类也是依然可以进行使用 的。
1.子类对象在创建时会先调用父类构造函数
2.父类构造函数执行结束后,执行子类构造函数
3.当父类构造函数有参数时,需要子类初始化列表中显式调用
4.析构函数的调用顺序与构造函数刚好相反。
先构造父类,再构造成员变量,最后再构造自己
先析构自己,再析构成员变量,最后析构父类
继承总结
1、当子类成员变量与父类成员变量同名时
2、子类依然从父类继承同名成员
3、在子类中通过作用域分辨符::进行同名成员区分(在派生类中使用基
类的同名成员,显式地使用类名限定符)
4、同名成员存储在内存中的不同位置
cout<<c.age<<endl;
cout<<c.Parent::age<<endl;
cout<<c.Child::age<<endl;
派生类中的static关键字
基类定义的静态成员,将被所有派生类共享
根据静态成员自身的访问特性和派生类的继承方式,在类层次体系中具 有不同的访问性质 (遵守派生类的访问控制)
派生类中访问静态成员,用以下形式显式说明:
类名::成员、对象名 . 成员
静态成员变量,无论使用父类名,还是子类名都可以直接调用得到。或者是实例,也可以调用。
cout<<c.count<<endl;
cout<<Parent::count;
Parent::count++;
cout<<Child::count;
Child::count++;
cout<<Object::count;
Object::count++;
cout<<Object::count;
多态
一般情况下,如果基类与子类有着同名的方法,即子类重写了父类的方法。如果使用父类指针去承接子类对象,那么调用被重写的方法的时候,是执行父类的方法,而不是子类的方法。
如何实现,根据对象实际类型去打印?virtual 可以解决。通过使用virtual 修饰函数。可以视作是java中的abstract 抽象方法,不过这个virutal方法在是可以有方法体的。
virtual int fun()
多态成立的3个条件。
1.要有继承
2.要有虚函数重写
3.要有父类指针(父类引用)指向子类对象。
静态联编 动态联编
静态联编,由于程序没有运行,所以不可能知道父类指针指向的具体对象是父类对象还是子类对象,从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译结果为调用父类的成员函数。
多态是发生在动态联编,是在程序执行的时候,判断父类指针应该调用的方法。
虚析构函数
虚析构函数用于指引delete操作符正确析构动态对象。
在手动delete obj时候,如果是父类指针,没有写virutal 析构,那么只会执行父类的析构函数代码,而不会执行子类的析构函数。
重载 重写 重定义
函数重载
必须在同一个类中进行,子类无法重载父类的函数,父类同名函数将被名称覆盖,重载是在编译期间根据参数类型和个数决定函数调用
//以下都是重载
void a();
void a(int)
void a(char)
void a(int,char)
函数重写
必须发生在父类与子类之间 ,并且父类与子类必须有完全相同的原型
使用virtual声明之后能够产生多态(如果不使用virutal ,那就叫重定义)
virtual void fun();
-->
void fun();//多态如果没有virutal 那就是重定义
虚函数表和Vptr指针
1.当类声明虚函数时,编译器会生成一个虚函数表
2.虚函数表是一个存储类成员函数指针 的数据结构
3.虚函数表是 编译器自动生成和维护的。
4.virtual 成员函数会被编译进入虚函数表中。
5.存在虚函数的时候,每个对象中都有一个指向虚函数表的指(vptr指针。)
编译器确定func 是不是虚函数
1)不是,编译器可直接确定被调用的成员函数(静态联编,根据父类类型决定)
2) 是,编译器根据对象的vptr指针,所指向虚函数表查询到func 函数并且调用。注意,查找与调用是在运行的时候执行
由于虚函数是在运行的时候动态进行调用。效率低是一个问题。构造函数无法实现多态,父类构造的时候,vptr 指向父类的虚函数表,子类构造时候,指向子类。
base(){
this->print()///调用的是父类的print方法
}
child(){
this->print()//调用的是子类的print 方法
}
纯虚函数
virtual 类型 函数名(参数表) = 0;
类似于java 的抽象方法。主要特点是无方法体。子类继承必须实现,否则子类也是抽象类’
1.含有纯虚函数的类,称为抽象基类,不可实列化。即不能创建对象,存在 的意义 就是被继承,提供族类的公共接口。
2,纯虚函数只有声明,没有实现,被“初始化”为 0。
3,如果一个类中声明了纯虚函数,而在派生类中没有对该函数定义,则该虚函数在 派生类中仍然为纯虚函数,派生类仍然为纯虚基类。
class Triangle:public Shape{
protected:
double w;
double h;
public:
Triangle(double w,double h):w(w),h(h){}
virtual double getArea() {
return w*h/2;
};
};
class Rectangle:public Triangle{
public:
Rectangle(double w,double h):Triangle(w,h){
}
double getArea(){
//可以调用到父类方法,只不过需要加上作用范围。
return this->Triangle::getArea()*2;
}
};
C 语言面向接口编程
c 语言通过函数指针实现面向接口编程
关于模板类使用友元重载<<的部分说明
如果单独把代码写在h文件里面,此时需要注意的细节会比较少,但是如果需要分文件,即使h cpp
如下只是重载了<<运算符。
friend ostream& operator<< <T>(ostream &o,Animal<T> t);
//此时,在h文件头我们需要做如下声明
template<class T>
class Animal;
template<class T>
ostream& operator<<(ostream &o,Animal<T> t);
//同时,我们需要在cpp文件里面完善代码。
template<class T>
ostream& operator<<(ostream &o,Animal<T> t){
o<<t.t;
return o;
}
//最后,在main里面,别忘记了,要引入我们的cpp文件。
所以这也是为什么我们对于这些模板类的编写,一般就用一个文件hpp,我们不把代码分离开,因为如果分开用户在使用的时候,还需要手工引入hpp,多麻烦啊。所以干脆写在一个文件里面算了。
一般性结论
friend ostream & operator<< <T> (ostream &os, Complex<T> &c);
//在模板类中 如果有友元重载操作符<<或者>>需要 在 operator<< 和 参数列表之间 //加⼊入
//滥⽤用友元函数,本来可以当成员函数,却要⽤用友元函数
//如果说是⾮非<< >> 在模板类中当友元函数
//在这个模板类 之前声明这个函数
friend Complex<T> mySub <T>(Complex<T> &one, Complex<T> &another);
//最终的结论, 模板类 不要轻易写友元函数, 要写的 就写<< 和>> 。
传说中的hpp
类型转换
C++风格的类型转换提供了 4 种类型转换操作符来应对不同场合的应用。
static_cast 静态类型转换。如 int 转换成 char
reinterpreter_cast 重新解释类型
dynamic_cast 命名上理解是动态类型转换。如子类和父类之间的多态类型转换。 const_cast, 字面上理解就是去 const 属性。
由于二次编译,模板类在.h在第一次编译之后,并没有最终确定类的具 体实现,只是编译器的词法校验和分析。在第二次确定类的具体实现后,是 在.hpp文件生成的最后的具体类,所以main函数需要引入.hpp文件。
综上:引入hpp文件一说也是曲线救国之计,所以实现模板方法建议在同 一个文件.h中完成
异常
捕捉万能异常?
//设置未知异常处理回调函数
set_terminate(my_tm_h);
//typedef void (*terminate_handler)();
//编写回调函数
void my_tm_h(){
cout<<"error occure with clear reason!"<<endl;
}
统一的异常处理机制,将让我们捕捉异常的时候,更加方便。能够进行统一的控制操作。
关于标准输入输出流
标准输入流对象cin
cin.get() //一次只能读取一个字符
cin.get(一个参数) //读一个字符
cin.get(三个参数) //可以读字符串
cin.getline() //获取缓冲区的一行
cin.ignore() //跳过缓冲区几个字符
cin.peek() //查看缓冲区有没有数据,阻塞
cin.putback() //塞进去缓冲区
ctrl-z 将产生一个EOF,mac 测试无效。
标准输出流对象cout
控制符详解
控制符,可以直接cout<<控制符,也就是追加
控制符 | 作用 |
---|---|
endl | 不做解释 |
dec | 设置数字基数为10进制 |
hex | 16进制 |
oct | 8进制 |
setfill(’*’) | 填充字符,一般与setw 配合使用,一次性用,endl作废 |
setprecision(n) | 设置浮点数字的输出精度,多次有效 |
setw(12) | 设置字段宽度,本次有效。仅仅数字不足才补足,一旦设置setprecision将优先填充到precision 的位数 |
setiosflags(ios::a|ios::b…) | 可以设置标志位 |
resetiosflags(ios::a|ios::b…) | 重置io标志 |
标志位作用讲解
标志位 | 作用 |
---|---|
ios::fixed | 有效数字位数默认为为6位,可以通过setprecision来定制。 |
ios::scientific | 设置浮点数以科学计数法表示 |
ios::left | 左对齐,仅仅setw ,并且字符串长度超过有效 。 |
ios:right | 右对齐 |
ios::skipws | skipws是作用于流式输入的,而非输出。 cin默认是已经把skipws开启了。 也就是说 a b c 读入三个字符,不会吧空格读进去。 |
ios::uppercase | 只有16进制输出才以大写输出 |
ios::lowercase | 小写输出 |
ios::showpos | 输出正数给出“+”号 |
ios::internal | 数值的符号位在域宽左对齐,数值右对齐,中间填充字符 |
ios:unitbuf | 每次输出后刷新所有的流 |
ios::stdio | 每次输出后清除stdout,stderr |
流成员函数
如下方法属于cout成员,使用cout.method进行调用即可。
函数 | 功能 |
---|---|
precision(n) | 设置精度 |
width(n) | 设置字符宽度 |
fill© | 设置填充字符 |
setf() | 设置ios标志 |
unsetf() | 取消设置ios标志 |
文件流
引入头文件
#include <fstream>
文件的输入输出方式控制
STL
常见的容器有如下的几种
vector list deque set map stack queue
string
如何写一个标准的替换字符串函数
string str="jflkasfijfamsfkajsaf";
string find="j";
string rep="*";
int pos=0;
while((pos=str.find(find,pos))!=-1){
str.replace(pos,find.length(),rep);
pos+=rep.length();
}
vector
deque
与vector 不同在于,deque 可以push_front,即双端操作。pop_front()可以取除第一个元素
stack
支持push 与pop ,通过top 获取元素
queue
队列,pop时候,只会把队头元素抛出。支持front 与end
list
不可随机存储读写,但是支持插入?支持push pop back front
总结一句话,能够用下标获取,其底层机构一定是数组,如果无法,但是可以通过不断的++ ,那他一定是一条链路。
各个容器的使用正确时机。
简单的总结
deque的使用场景:比如排队购票系统,对排队者的存储可以采用deque,支持头 端的快速移除,尾端的快速添加。如果采用 vector,则头端移除时,会移动大量的 数据,速度慢。
- vector 与 deque 的比较:
- 一:vector.at()比 deque.at()效率高,比如 vector.at(0)是固定的,deque 的开始位置却
是不固定的。 - 二:如果有大量释放操作的话,vector 花的时间更少,这跟二者的内部实现有关。
- 三:deque 支持头部的快速插入与快速移除,这是 deque 的优点。
- list 的使用场景:比如公交车乘客的存储,随时可能有乘客下车,支持频繁的不确实
位置元素的移除插入。 - set 的使用场景:比如对手机游戏的个人得分记录的存储,存储要求从高分到低分的
顺序排列。 - map 的使用场景:比如按 ID 号存储十万个用户,想要快速要通过 ID 查找对应的用
户。二叉树的查找效率,这时就体现出来了。如果是 vector 容器,最坏的情况下可 能要遍历完整个容器才能找到该用户。