0%

c++语法笔记

补充点

引用(&)

&在C语言表示的是取地址符————用在函数传参中的指针赋值

在C++语言中引用是某一个变量的别名,对引用的操作与对变量直接操作完全一样。

1
2
int a;
int &ra=a;//定义引用ra,它是变量a的引用,即别名
  • &在此不是求地址运算,而是起标识作用。
  • 声明引用时,必须同时对其进行初始化。
  • 类型标识符是指目标变量的类型。
  • 引用声明完毕后,相当于目标变量名有两个名称————目标原名和引用名,且不能再把该引用名作为其他变量名的别名
  • 引用本身不占存储单元,本身不是一种数据类型
  • 不能建立数组的引用

引用的作用

  • 作为函数的参数,效果是和传递指针的效果是一样的
  • 使用引用传递函数的参数,由于在内存中没有产生实参的副本,它是直接对实参操作

常引用

1
2
3
4
5
常引用声明方式:const 类型标识符 &引用名=目标变量名;
int a ;
const int &ra=a;
ra=1; //错误
a=1; //正确

不能通过引用对目标变量的值进行修改,从而使引用的目标成为const

引用作为返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream.h>
float temp; //定义全局变量temp
float fn1(float r); //声明函数fn1
float &fn2(float r); //声明函数fn2
float fn1(float r) //定义函数fn1,它以返回值的方法返回函数值
{
 temp=(float)(r*r*3.14);
 return temp;
}
float &fn2(float r) //定义函数fn2,它以引用方式返回函数值
{
 temp=(float)(r*r*3.14);
 return temp;
}
void main() //主函数
{
 float a=fn1(10.0); //第1种情况,系统生成要返回值的副本(即临时变量)
 float &b=fn1(10.0); //第2种情况,可能会出错(不同 C++系统有不同规定)
 //不能从被调函数中返回一个临时变量或局部变量的引用
 float c=fn2(10.0); //第3种情况,系统不生成返回值的副本
 //可以从被调函数中返回一个全局变量的引用
 float &d=fn2(10.0); //第4种情况,系统不生成返回值的副本
 //可以从被调函数中返回一个全局变量的引用
 cout<<a<<c<<d;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream.h>
int &put(int n);
int vals[10];
int error=-1;
void main()
{
put(0)=10; //以put(0)函数值作为左值,等价于vals[0]=10;
put(9)=20; //以put(9)函数值作为左值,等价于vals[9]=20;
cout<<vals[0];
cout<<vals[9];
}
int &put(int n)
{
if (n>=0 && n<=9 ) return vals[n];
else { cout<<"subscript error"; return error; }
}

引用和多态

1
2
3
4
class  A;
class  Bpublic A{……};
B  b;
A  &Ref = b; // 用派生类对象初始化基类对象的引用

Ref 只能用来访问派生类对象中从基类继承下来的成员,是基类引用指向派生类。如果A类中定义有虚函数,并且在B类中重写了这个虚函数,就可以通过Ref产生多态效果。

虚函数

抽象类是指包括至少一个纯虚函数的类。

C++允许用户使用虚函数 (virtual function) 来完成 运行时决议 这一操作,这与一般的 编译时决定 有着本质的区别

  • 在基类用virtual声明成员函数为虚函数。这样就可以在派生类中重新定义此函数,为它赋予新的功能,并能方便被调用。
    • 在派生类中重新定义此函数,要求函数名,函数类型,函数参数个数和类型全部与基类的虚函数相同,并根据派生类的需要重新定义函数体。
  • 在类外定义虚函数时,不必在定义virtual
  • c++规定,当一个成员函数被声明为虚函数后,其派生类的同名函数都自动成为虚函数。因此在派生类重新声明该虚函数时,可以加virtual,也可以不加,但习惯上一般在每层声明该函数时都加上virtual,使程序更加清晰。
  • 如果再派生类中没有对基类的虚函数重新定义,则派生类简单的继承起基类的虚函数。

实现机制

用虚表和虚指针

每个类用了一个虚表,每个类的对象用了一个虚指针。虚表是和类对应的,虚表指针是和对象对应的。

虚表

  • 一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数

  • 没有覆盖父类的虚函数是毫无意义的。

多态

C++中的虚函数的作用主要是实现了多态的机制。

多态用虚函数来实现,结合动态绑定此时的调用就不会在编译时候确定而是在运行时确定。不在单独考虑指针/引用的类型而是看指针/引用的对象的类型来判断函数的调用,根据对象中虚指针指向的虚表中的函数的地址来确定调用哪个函数

虚函数是在基类中定义的,目的是不确定它的派生类的具体行为。故可以理解成java的父类中待覆写的函数

混淆点

关于函数的调用机制

(1)调用过程:建立函数调用栈—保存调用函数的运行状态和返回地址—传参—进入被调函数。

(2)传参机制:传参不一定会压栈!fastcall(快调)一般会由寄存器传参,另外当参数不超过4个时一般也由寄存器传参,否则会压栈。压栈顺序一般是从右到左

传参方式

值传递(传副本)

形参只是实参的拷贝

在函数结束时,形参作为局部变量会被释放,对实参不会产生任何影响。若为类的对象会调用拷贝构造,这种深拷贝操作会影响到传参效率。(可理解为“单向接口”)

指针传递

指针本质上是一个变量,该变量的值是一个地址,指针在逻辑上是独立的,可以被改变。

传递的是实参的地址,所以函数内部对形参得操作会“同步更新”到实参。(可理解为“双向接口”)

引用传递

传递的是实参的别名,传参时形参被绑定到实参对象上,因此函数内部对形参的操作也都会“同步更新”到源实参。(可理解为“双向接口”)

引用传递和值传递的异同

相同点

  • 都是地址的概念

不同点

  • 指针是一个实体(替身);引用只是一个别名(本体的另一个名字)

  • 引用只能在定义时被初始化一次,之后不可改变,即“从一而终”;指针可以修改,即“见异思迁”;

  • 引用不能为空(有本体,才有别名);指针可以为空;

  • sizeof 引用,得到的是所指向变量的大小;sizeof 指针,得到的是指针的大小;

  • 指针 ++,是指指针的地址自增;引用++是指所指变量自增;

  • 引用是类型安全的,引用过程会进行类型检查;指针不会进行安全检查;

关于static用法

静态局部变量!=全局变量,二者生命周期相同,但作用域不同,静态局部变量只对函数体内部可见。

static修饰全局变量:限定该变量只在该文件中可用。

static修饰外部函数:往往在函数声明中加static修饰,限定该函数只在该文件可用(若在头文件中声明,则限定只在其对应的源文件中可用)

类中static修饰的成员。静态成员与类的对象实体无关,是该类的共享变量

关于extern用法

C/C++头文件中的函数声明默认为extern,即外部可用(其他源文件只需包含头文件即可使用)。和static修饰的效果相反。

带extern的变量仅仅是声明而不是定义!用extern使变量可以在多文件中共享,主要有两种做法:

  • 在源文件中定义,其他需要使用该变量的源文件用extern声明。(表示该变量在其它文件中定义,即一次定义,多次extern声明)
  • 在源文件中定义,其对应的头文件中extern声明,其他需要使用该变量的源文件包含该头文件即可。(更加标准的做法)

内联

用途:定义或声明函数时返回值前加上inline修饰能避免函数调用带来的时间和空间开销,提高效率,适用于反复执行的核心代码。内部实现机制其实是编译时按函数体展开代码,避免了函数调用的一系列压栈出栈过程。

限制:

​ (1)不能出现复杂的控制结构语句;

​ (2)递归函数不能用作内联函数;

​ (3)内联函数体不宜代码过长,只适合数行的小函数。

注:1.内联和宏定义均属于代码替换机制,但前者安全性更好,宏只是预处理做简单的符号替换而不会做类型检查。内联可以完全替代宏,反之不能。

​ 2.inline关键字只是一种“建议”,是否采用内联机制取决于编译器。

重载

用途:C++特有机制。让同一种算法针对不同类型使用相同的函数名,提高代码可读性,重载的函数至少在参数个数或类型上有所区别,仅返回值区别不能重载!

1
2
void func(int);和int func(int);//编译器无法区分
int abs(int);和double abs(double);//C++通过名字粉碎技术自动匹配

ps:重写是面向对象中子类对父类虚函数的重新实现

底层const修饰的参数以及常量成员函数也可以重载。

友元

  • 友元(frend)机制允许一个类将对其非公有成员的访问权授予指定的函数或者类
  • 友元的声明以friend开始,它只能出现在类定义的内部

友元函数

友元函数是一个不属于类成员的函数,但它可以访问该类的私有成员。————友元函数视作好像是该类的一个成员

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#include <iostream>

using namespace std;

class A
{
public:
friend void set_show(int x, A &a); //该函数是友元函数的声明
private:
int data;
};

void set_show(int x, A &a) //友元函数定义,为了访问类A中的成员
{
a.data = x;
cout << a.data << endl;
}
int main(void)
{
class A a;

set_show(1, a);

return 0;
}

友元类

友元类的所有成员函数都是另一个类的友元函数,都可以访问另一个类中的隐藏信息(包括私有成员和保护成员)。

关于友元类的注意事项:

(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
(3) 友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的申明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>

using namespace std;

class A
{
public:
friend class C; //这是友元类的声明
private:
int data;
};

class C //友元类定义,为了访问类A中的成员
{
public:
void set_show(int x, A &a) { a.data = x; cout<<a.data<<endl;}
};

int main(void)
{
class A a;
class C c;

c.set_show(1, a);

return 0;
}

友元成员函数

使类B中的成员函数成为类A的友元函数,这样类B的该成员函数就可以访问类A的所有成员了。

vector用法

vector 是向量类型,它可以容纳许多类型的数据,如若干个整数,所以称其为容器。

vector是一个类模板,而不是类型。

动态联编与静态联编

c++一般的子类和父类继承关系的时候,都是使用的是静态联编(和之前学习的指针的用法一致)参考:https://blog.csdn.net/neiloid/article/details/6934129

参考:https://blog.csdn.net/neiloid/article/details/6934129

参考:https://blog.csdn.net/gaoxin1076/article/details/8298279

联编是指一个计算机程序自身彼此关联的过程,在这个联编过程中,需要确定程序中的操作调用(函数调用)与执行该操作(函数)的代码段之间的映射关系;按照联编所进行的阶段不同,可分为静态联编和动态联编;

仔细读读红色字体的那部分句子。我们就能很清楚的明白什么是联编了。给大家举个最通俗易懂的例子好了:

A类中有fun这个函数, B类中也有fun这个函数,现在我在类外的main函数里面调用fun 函数。

那么main函数就是函数调用,调用fun函数,

而A类中的fun函数和B类中的fun函数就是执行该操作的代码段

所以现在联编就是实现两者的映射关系。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class A
{
void func() {cout<<"It's A"<<endl;

};

class B
{
void func() {cout<<"It's B"<<endl;

};
int main()
{
func();
}

联编就是决定将main函数中的func()的函数调用映射到A中的func函数还是B中的func函数的过程。

2.静态联编 和 动态联编 的定义

知道了什么事联编,那么再来理解动态联编 和静态联编也就不难了

静态联编:
是指联编工作是在程序编译连接阶段进行的,这种联编又称为早期联编;因为这种联编是在程序开始运行之前完成的;
在程序编译阶段进行的这种联编又称静态束定;在编译时就解决了程序中的操作调用与执行该操作代码间的关系,确定这种关系又被称为束定;编译时束定又称为静态束定;

拿上面的例子来说,静态联编就是在编译的时候就决定了main函数中调用的是A中的func还是B中的func。一旦编译完成,那么他们的映射关系就唯一确定了。

动态联编:
编译程序在编译阶段并不能确切地知道将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切地知道将要调用的函数,要求联编工作在程序运行时进行,这种在程序运行时进行的联编工作被称为动态联编,或动态束定,又叫晚期联编;C++规定:动态联编是在虚函数的支持下实现的;

动态联编在编译的时候还是不知道到底应该选择哪个func函数,只有在真正执行的时候,它才确定。

静态联编和动态联编都是属于多态性的,它们是在不同的阶段进对不同的实现进行不同的选择;

也可以这么说:C++多态有两种形式,动态多态和静态多态(函数重载);动态多态是指一般的多态,是通过类继承和虚函数机制实现的多态;静态多态是通过模板来实现,因为这种多态实在编译时而非运行时,所以称为静态多态。

3.静态联编

首先还是拿个例子来说事吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <iostream>
using namespace std;
class shape{
public:
void draw(){cout<<"I am shape"<<endl;}
void fun(){draw();}
};
class circle:public shape{
public:
void draw(){cout<<"I am circle"<<endl;}
};
void main(){
circle oneshape;
oneshape.fun();
}

现在我们详细具体定义了一开始的A类和B类以及func函数。让我们来分析一下:
调用oneshape.fun()的时候,进入类shape中的fun函数。

现在我们的问题就是:fun函数调用的draw到底是shape里面的draw还是circle中的draw??

答案是:它调用了cshape这个基类的draw函数。所以输出了 I am shape

那么一直困扰我的问题是:为什么调用基类的draw而不是派生类中得draw呢?

书上好像没有具体讲,上课的时候老师那也根本不会讲。

自己想了一下,应该可以从汇编的角度理解:

1.调用oneshape.fun(),这里是一个跳转指令,进入类shape中的fun函数所在的代码段

2.类shape的代码段是依次顺序放置的。进入fun函数后,现在我们要调用draw的地址。

由于没有另外的数据结构来保存draw的地址,所以程序所知道的,必然只有在shape类中的draw地址了,仅仅用一个跳转指令

在我的vs2010的反汇编调试窗口下是这样的一句代码:

013B1546 call shape::draw (13B10F5h)

很明确这里指出了shape::draw,也就确定了映射关系,完成了联编。

C++和java的异同

  1. Java源码会先经过一次编译,成为中间码,中间码再被解释器解释成机器码。对于Java而言,中间码就是字节码(.class),而解释器在JVM中内置了。

    C++源码一次编译,直接在编译的过程中链接了,形成了机器码。

  2. Java是纯面向对象的语言,所有代码(包括函数、变量)都必须在类中定义。而C++中还有面向过程的东西,比如是全局变量和全局函数。

  3. C++支持多继承,Java中类都是单继承的。但是继承都有传递性,同时Java中的接口是多继承,类对接口的实现也是多实现。

变量和类型

  • Java没有无符号整数。(无符号右移在Java中强制用三个右尖括号表示)。

  • Java有内置类型String,而C++没有。C++的std::string是可变的,类似于Java的StringBuffer。

  • Java中不存在指针。Java的引用是功能弱化的指针,只能做“调用所指对象的方法”的操作。

  • Java中,对象只能由引用传递,C++中对象可由值或引用传递。

类机制

  • Java是完全面向对象的,所有方法都必须写在类中。

C++标准库

IO库