C++对C的扩展

1. 程序设计方法的发展历程

面向过程的结构化程序设计方法

  • 设计思路:
    — 自顶向下、逐步求精。采用模块分解与功能抽象,自顶向下、分而治之。
  • 程序结构:
    — 按功能划分为若干个基本模块,形成一个树状结构。
    — 各模块间的关系尽可能简单,功能上相对独立;每一模块内部— 均是由顺序、选择和循环三种基本结构组成。
    — 其模块化实现的具体方法是使用子程序。
  • 优点:
    有效地将一个较复杂的程序系统设计任务分解成许多易于控制和处理的子任务,便于开发和维护。
  • 缺点:
    可重用性差、数据安全性差、难以开发大型软件和图形界面的应用软件
    –把数据和处理数据的过程分离为相互独立的实体。
    –当数据结构改变时,所有相关的处理过程都要进行相应的修改。
    –每一种相对于老问题的新方法都要带来额外的开销。
    –图形用户界面的应用程序,很难用过程来描述和实现,开发和维护也都很困难。

面向对象的方法

  • 将数据及对数据的操作方法封装在一起,作为一个相互依存、不可分离的整体——对象。
  • 对同类型对象抽象出其共性,形成类。
  • 类通过一个简单的外部接口,与外界发生关系。
  • 对象与对象之间通过消息进行通信。
面向对象的基本概念

对象:

  • 一般意义上的对象:
    –是现实世界中一个实际存在的事物。
    –可以是有形的(比如一辆汽车),也可以是无形的(比如一项计划)。
    –是构成世界的一个独立单位,具有
    1. 静态特征:可以用某种数据来描述
    2. 动态特征:对象所表现的行为或具有的功能
  • 面向对象方法中的对象:
    –是系统中用来描述客观事物的一个实体,它是用来构成系统的一个基本单位。对象由一组属性和一组行为构成。
    –属性:用来描述对象静态特征的数据项。
    行为:用来描述对象动态特征的操作序列。

类:

  • 分类——人类通常的思维方法

  • 分类所依据的原则——抽象
    –忽略事物的非本质特征,只注意那些与当前目标有关的本质特征,从而找出事物的共性,把具有共同性质的事物划分为一类,得出一个抽象的概念。
    –例如,石头、树木、汽车、房屋等都是人们在长期的生产和生活实践中抽象出的概念。

  • 面向对象方法中的"类"
    –具有相同属性和服务的一组对象的集合
    –为属于该类的全部对象提供了抽象的描述,包括属性和行为两个主要部分。
    –类与对象的关系:
    犹如模具与铸件之间的关系,一个属于某类的对象称为该类的一个实例。

封装:
也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。

  • 把对象的属性和服务结合成一个独立的系统单元。
  • 尽可能隐蔽对象的内部细节。对外形成一个边界(或者说一道屏障),只保留有限的对外接口使之与外部发生联系。

继承:

  • 继承对于软件复用有着重要意义,是面向对象技术能够提高软件开发效率的重要原因之一。
  • 定义:特殊类的对象拥有其一般类的全部属性与服务,称作特殊类对一般类的继承
  • 例如:将轮船作为一个一般类,客轮便是一个特殊类。

多态 :
多态是指在一般类中定义的属性或行为,被特殊类继承之后,可以具有不同的数据类型或表现出不同的行为。这使得同一个属性或行为在一般类及其各个特殊类中具有不同的语义。

2. C++对C的加强

2.1 namespace命名空间

1. C++命名空间基本常识

所谓namespace,是指标识符的各种可见范围。C++标准程序库中的所有标识符都被定义于一个名为std的namespace中。
一 :<iostream>和<iostream.h>格式不一样,前者没有后缀,实际上,在你的编译器include文件夹里面可以看到,二者是两个文件,打开文件就会发现,里面的代码是不一样的。后缀为.h的头文件c++标准已经明确提出不支持了,早些的实现将标准库功能定义在全局空间里,声明在带.h后缀的头文件里,c++标准为了和C区别开,也为了正确使用命名空间,规定头文件不使用后缀.h。 因此,
1)当使用<iostream.h>时,相当于在c中调用库函数,使用的是全局命名空间,也就是早期的c++实现;
2)当使用<iostream>的时候,该头文件没有定义全局命名空间,必须使用namespace std;这样才能正确使用cout。
二: 由于namespace的概念,使用C++标准程序库的任何标识符时,可以有三种选择:
1、直接指定标识符。例如std::ostream而不是ostream。完整语句如下: std::cout << std::hex << 3.4 << std::endl;
2、使用using关键字。 using std::cout; using std::endl; using std::cin; 以上程序可以写成 cout << std::hex << 3.4 << endl;
3、最方便的就是使用using namespace std; 例如: using namespace std;这样命名空间std内定义的所有标识符都有效(曝光)。就好像它们被声明为全局变量一样。那么以上语句可以如下写: cout <<hex << 3.4 << endl;因为标准库非常的庞大,所以程序员在选择的类的名称或函数名 时就很有可能和标准库中的某个名字相同。所以为了避免这种情况所造成的名字冲突,就把标准库中的一切都被放在名字空间std中。但这又会带来了一个新问 题。无数原有的C++代码都依赖于使用了多年的伪标准库中的功能,他们都是在全局空间下的。所以就有了<iostream.h> 和<iostream>等等这样的头文件,一个是为了兼容以前的C++代码,一个是为了支持新的标准。命名空间std封装的是标准程序库的名称,标准程序库为了和以前的头文件区别,一般不加".h"

2. C++命名空间定义及使用语法

在C++中,名称(name)可以是符号常量、变量、宏、函数、结构、枚举、类和对象等等。为了避免,在大规模程序的设计中,以及在程序员使用各种各样的C++库时,这些标识符的命名发生冲突,
标准C++引入了关键字namespace(命名空间/名字空间/名称空间/名域),可以更好地控制标识符的作用域。

std是c++标准命名空间,c++标准程序库中的所有标识符都被定义在std中,比如标准库中的类iostream、vector
等都定义在该命名空间中,使用时要加上using声明(using namespace std) 或using指示(如std::string、
std::vector<int>).

  • C中的命名空间
    在C语言中只有一个全局作用域
    C语言中所有的全局标识符共享同一个作用域
    标识符之间可能发生冲突
  • C++中提出了命名空间的概念
    命名空间将全局作用域分成不同的部分
    不同命名空间中的标识符可以同名而不会发生冲突
    命名空间可以相互嵌套
    全局作用域也叫默认命名空间

3.register 关键字增强

register关键字 请求编译器让变量a直接放在寄存器里面,速度快
在c语言中 register修饰的变量 不能取地址,但是在c++里面做了内容

  1. register关键字的变化
    register关键字请求“编译器”将局部变量存储于寄存器中
    C语言中无法取得register变量地址
    在C++中依然支持register关键字
    C++编译器有自己的优化方式,不使用register也可能做优化
    C++中可以取得register变量的地址
  2. C++编译器发现程序中需要取register变量的地址时,register对变量的声明变得无效。
  3. 早期C语言编译器不会对代码进行优化,因此register变量是一个很好的补充。

4.变量检测增强

在C语言中,重复定义多个同名的全局变量是合法的
在C++中,不允许定义多个同名的全局变量

struct类型加强

struct类型的加强:
C语言的struct定义了一组变量的集合,C编译器并不认为这是一种新的类型
C++中的struct是一个新类型的定义声明

struct Student
{
    char name[100];
    int age;
};

int main(int argc, char *argv[])
{
    Student s1 = {"wang", 1};
    Student s2 = {"wang2", 2};    
    return 0;
}
C++中所有的变量和函数都必须有类型

C++中所有的变量和函数都必须有类型
C语言中的默认类型在C++中是不合法的
函数f的返回值是什么类型,参数又是什么类型?
函数g可以接受多少个参数?

f(i)
{
    printf("i = %d\n", i);

}

g()
{
    return 5;
}

int main(int argc, char *argv[])
{

    f(10);

    printf("g() = %d\n", g(1, 2, 3, 4, 5));


    getchar();  
    return 0;
}
  • 在C语言中
    int f( );表示返回值为int,接受任意参数的函数
    int f(void);表示返回值为int的无参函数
    在C++中
    int f( );和int f(void)具有相同的意义,都表示返回值为int的无参函数
  • C++更加强调类型,任意的程序元素都必须显示指明类型
新增Bool类型关键字
  • C++中的布尔类型
    C++在C语言的基本类型系统之上增加了bool
    C++中的bool可取的值只有true和false
    理论上bool只占用一个字节,
    如果多个bool变量定义在一起,可能会各占一个bit,这取决于编译器的实现
    true代表真值,编译器内部用1来表示
    false代表非真值,编译器内部用0来表示
    bool类型只有true(非0)和false(0)两个值
    C++编译器会在赋值时将非0值转换为true,0值转换为false
三目运算符功能增强
(a < b ? a : b )= 30;

1)C语言返回变量的值 C++语言是返回变量本身
C语言中的三目运算符返回的是变量值,不能作为左值使用
C++中的三目运算符可直接返回变量本身,因此可以出现在程序的任何地方
2)注意:三目运算符可能返回的值中如果有一个是常量值,则不能作为左值使用
(a < b ? 1 : b )= 30;

5. C/C++中的const

1. const基础知识(用法、含义、好处)
nt main()
{
const int a;
int const b;

const int *c;
int * const d;
const int * const e ;

return 0;
}

初级理解:const是定义常量==》const意味着只读
含义:

第一个第二个意思一样 代表一个常整形数
第三个 c是一个指向常整形数的指针(所指向的内存数据不能被修改,但是本身可以修改)
第四个 d 常指针(指针变量不能被修改,但是它所指向内存空间可以被修改)
第五个 e一个指向常整形的常指针(指针和它所指向的内存空间,均不能被修改)

2. C中“冒牌货”
int main()
{
    const int a = 10;
    int *p = (int*)&a; 
    printf("a===>%d\n", a);
    *p = 11;
    printf("a===>%d\n", a);

    printf("Hello......\n");
    return 0;
}

解释:

以上代码在C语言中是可行的,可以通过指针修改a的值, 说明a只是一个只读的变量。通过a变量名访问内存是只读的,但是可以通过其他方式访问
在C++中,以上代码int *p = (int*)&a; 直接编译报错。

  • 在C++中,用符号表代替了只读内存的方式;

C++中const符号表原理图


C++中const符号表

注意:
C++编译器虽然可能为const常量分配空间,但不会使用其存储空间中的值。

3. const和#define相同之处

都可以作为常量使用

4.const和#define的区别
  • const常量是编译运行阶段使用,编译器对其进行类型和作用域检查。
  • define 在预编译阶段进行宏展开,不做类型检查
  • const 只能是常量,而define可以是表达式
5. 结论
  • C语言中的const变量
    C语言中const变量是只读变量,有自己的存储空间
    C语言中的const常量是全局的,如果别的文件有相同名字的定义会出现符号重定义错误
    *C++中的const常量
    可能分配存储空间,也可能不分配存储空间 ,但是将其存放到了符号表里,只能从符号表里获取值。
    C++中const常量是本文件的作用域,如果需要在别的文件访问 需要extent声明
    当const常量为全局,并且需要在其它文件中使用,会分配存储空间

当使用&操作符,取const常量的地址时,会分配存储空间
当const int &a = 10; const修饰引用时,也会分配存储空间

5. 引用专题讲座

1. 引用的概念

a) 在C++中新增加了引用的概念
b) 引用可以看作一个已定义变量的别名
c) 引用的语法:Type& name = var;
d) 引用做函数参数那?(引用作为函数参数声明时不进行初始化)

void main01()
{
    int a = 10; //c编译器分配4个字节内存。。。a内存空间的别名
    int &b = a;  //b就是a的别名。。。
    a =11; //直接赋值
    {
        int *p = &a;
        *p = 12;
        printf("a %d \n",a);
    }
    b  = 14; 
    printf("a:%d b:%d", a, b);
    system("pause");
}
2. 引用做函数参数
  • 普通引用在声明时必须用其它的变量进行初始化,
  • 引用作为函数参数声明时不进行初始化
struct Teacher
{
    char name[64];
    int age ;
};

void printfT(Teacher *pT)
{
    cout<<pT->age<<endl;
}

//pT是t1的别名 ,相当于修改了t1
void printfT2(Teacher &pT)
{
    //cout<<pT.age<<endl;
    pT.age = 33;
}

//pT和t1的是两个不同的变量
void printfT3(Teacher pT)
{
    cout<<pT.age<<endl;
    pT.age = 45; //只会修改pT变量 ,不会修改t1变量
}
void main()
{
    Teacher t1;
    t1.age = 35;

    printfT(&t1);

    printfT2(t1); //pT是t1的别名
    printf("t1.age:%d \n", t1.age); //33

    printfT3(t1) ;// pT是形参 ,t1 copy一份数据 给pT     //---> pT = t1
    printf("t1.age:%d \n", t1.age); //35
    
    cout<<"hello..."<<endl;    
    system("pause");
    return ;
}
3. 引用的意义

1)引用作为其它变量的别名而存在,因此在一些场合可以代替指针
2)引用相对于指针来说具有更好的可读性和实用性

int swap1(int &a, int &b)
{
   int t = a;
   a = b;
   b = t;
}

int swap2(int *a, int *b)
{
   int t = *a;
   *a = *b;
   *b = t;
}
4. 引用的本质

1)引用在C++中的内部实现是一个常指针
Type& name Type* const name
2)C++编译器在编译过程中使用常指针作为引用的内部实现,因此引用所占用的空间大小与指针相同。
3)从使用的角度,引用会让人误会其只是一个别名,没有自己的存储空间。这是C++为了实用性而做出的细节隐藏
可以通过编译查看汇编代码了解。

#include<iostream>

int main()
{
    int a = 10;
    int &b = a;
    return 0;
}
反汇编代码

可以看出引用也是一个指针。

5. 函数返回值是引用(引用当左值)

C++引用使用时的难点:

  • 当函数返回值为引用时
    若返回栈变量
    不能成为其它引用的初始值
    不能作为左值使用
  • 若返回静态变量或全局变量
    可以成为其他引用的初始值
    即可作为右值使用,也可作为左值使用
    C++链式编程中,经常用到引用,运算符重载专题

返回值是基础类型,当引用

int& getA1()
{
    int a;
    a = 10;
    return a;
}

int* getA2()
{
    int a;
    a = 10;
    return &a;
}

在某些编译器下 以上两种情况编译直接报错,栈变量(局部变量)不能作为返回值

返回值是static变量,当引用

int& getStaticA1()
{
    static int a = 10;
    a ++;
    printf("a:%d \n", a);
    return a;
}

int *getStaticA2()
{
    static int a = 10;
    a ++;
    printf("a:%d \n", a);
    return &a;
}

void main22()
{
    
    getStaticA1() = 100; //编译器帮我们打造了环境
    getStaticA1();
    *(getStaticA2()) = 200; //相当于我们程序员手工的打造 做左值的条件
    getStaticA2();
    system("pause");
}

可以看出静态变量作为引用或指针返回 可以做右值也能做左值。还可以被别的引用的初始值。

6. 指针引用
#include "iostream"
using namespace std;
struct Teacher
{
    char name[64];
    int age;
};
nt getTe(Teacher **myp )
{
    Teacher *p = (Teacher *)malloc(sizeof(Teacher));
    
    if (p ==NULL)
    {
        return -1;
    }
    memset(p, 0, sizeof(Teacher));
    p->age = 33;

    *myp  = p; //
    return 0;
}
//指针的引用而已
int getTe2(Teacher*  &myp)
{
    myp = (Teacher *)malloc(sizeof(Teacher));
    myp->age = 34;

    return 0;
}

void main333()
{
    Teacher *p = NULL;
    //getTe(&p);
    getTe2(p);

    printf("age:%d \n", p->age);
    system("pause");
}
7.常引用

在C++中可以声明const引用
const Type& name = var;
const引用让变量拥有只读属性

案例1:
int main()
{
    int a = 10;
    const int &b = a; 

    //int *p = (int *)&b;
    b = 11; //err
    //*p = 11; //只能用指针来改变了

    cout<<"b--->"<<a<<endl;
    printf("a:%d\n", a);
    printf("b:%d\n", b);
    printf("&a:%d\n", &a);
    printf("&b:%d\n", &b);
    system("pause");
    return 0;
}

在有些编译器中int *p = (int *)&b; 直接报错

8. 使用字面量常量初始化const引用

1、用变量对const引用初始化,const引用分配内存空间了吗?
2、用常量对const引用初始化,const引用分配内存空间了吗?

{
    const int b = 10;
    printf("b:%d", &b);

    //int &a1 = 19; 如果不加const编译失败
    const int &a = 19;
    printf("&a:%d \n", &a);

    system("pause");
}
9. const引用结论

1)const int &e 相当于 const int * const e;
2)int &e 相当于 int * const e;
3)当使用常量(字面量)对const引用进行初始化时,C++编译器会为常量值分配空间,并将引用名作为这段空间的别名
4)使用字面量对const引用初始化后,将生成一个只读变量

6. C++对C的函数扩展

1. inline内联函数
  • C++ 中的const常量可以代替宏常数定义,如:
    const int A = 3;  #define A 3
  • C++中是否有解决方案替代宏代码片段呢?(替代宏代码片段就可以避免宏的副作用!)
  • C++中推荐使用内联函数替代宏代码片段
  • C++中使用inline关键字声明内联函数

宏替换和函数调用区别如下:

#include "iostream"
using namespace std;
#define MYFUNC(a, b) ((a) < (b) ? (a) : (b))  

inline int myfunc(int a, int b) 
{
    return a < b ? a : b;
}

int main()
{
    int a = 1;
    int b = 3;
    //int c = myfunc(++a, b);  //头疼系统
    int c = MYFUNC(++a, b);  

    printf("a = %d\n", a); 
    printf("b = %d\n", b);
    printf("c = %d\n", c);

system("pause");
    return 0;
}

可以看出当用宏替换的话打印结果如下:

a = 3
b = 3
c = 3
这就是宏替换的副作用,因此用内联函数可以解决次问题

    1. 必须inline int myfunc(int a, int b)和函数体的实现,写在一块
    1. C++编译器直接将函数体插入在函数调用的地方
      内联函数没有普通函数调用时的额外开销(压栈,跳转,返回)
    1. 内联函数是一种特殊的函数,具有普通函数的特征(参数检查,返回类型等)
      内联函数是对编译器的一种请求,因此编译器可能拒绝这种请求
      内联函数由 编译器处理,直接将编译后的函数体插入调用的地方
      宏代码片段 由预处理器处理, 进行简单的文本替换,没有任何编译过程
    1. 现代C++编译器能够进行编译优化,因此一些函数即使没有inline声明,也可能被编译器内联编译
      另外,一些现代C++编译器提供了扩展语法,能够对函数进行强制内联
      如:g++中的 _attribute_((always_inline))属性

C++中内联编译的限制:
1) 不能存在任何形式的循环语句
2)不能存在过多的条件判断语句
3)函数体不能过于庞大 (1到5条语句为宜)
4)不能对函数进行取址操作
5)函数内联声明必须在调用语句之前
6)内联函数不能是递归函数
结论:如果不满足以上条件编译器将按普通函数编译。因为内联函数和普通函数的优势就是节省了开辟堆栈、压栈、跳转、返回的开销。如果内联函数的函数体过于庞大,开销远大于开辟堆栈、压栈、跳转、返回的开销。内联函数将没有意义。

2. 默认参数

C++中可以在函数声明时为参数提供一个默认值,
当函数调用时没有指定这个参数的值,编译器会自动用默认值代替

void myPrint(int x = 3)
{
    printf("x:%d", x);
}

函数默认参数的规则:

    1. 只有参数列表后面部分的参数才可以提供默认参数值
    1. 一旦在一个函数调用中开始使用默认参数值,那么这个参数后的所有参数都必须使用默认参数值
3. 函数占位参数

占位参数只有参数类型声明,而没有参数名声明
一般情况下,在函数体内部无法使用占位参数

int func(int a, int b, int ) 
{
    return a + b;
}

int main01()
{
    //func(1, 2); //可以吗?
    printf("func(1, 2, 3) = %d\n", func(1, 2, 3));

    getchar();  
    return 0;
}
4. 默认参数和占位参数

可以将占位参数与默认参数结合起来使用

  • 意义:
    为以后程序的扩展留下线索
    兼容C语言程序中可能出现的不规范写法
int func2(int a, int b, int = 0)
{
    return a + b;
}
void main()
{
    //如果默认参数和占位参数在一起,都能调用起来
    func2(1, 2);
    func2(1, 2, 3);
    system("pause");
}
  • 如果默认参数和占位参数在一起,都能调用起来
5. 函数重载(Overroad)

函数重载概念

    1. 函数重载概念
      函数重载(Function Overload)
      用同一个函数名定义不同的函数
      当函数名和不同的参数搭配时函数的含义不同
    1. 函数重载的判断标准
      函数重载至少满足下面的一个条件:
      参数个数不同
      参数类型不同
      参数顺序不同
      仅仅在返回值类型不同是不行的。
    1. 函数重载是发生在一个类中里面

函数重载遇上函数默认参数

//当函数默认参数遇上函数重载会发生什么

int func(int a, int b, int c = 0)
{
    return a * b * c;
}

int func(int a, int b)
{
    return a + b;
}

//1个参数的允许吗
int func(int a)
{
    return a + b;
}


int main()
{
    int c = 0;

    c = func(1, 2); // 存在二义性,调用失败,编译不能通过 

    printf("c = %d\n", c);

    printf("Press enter to continue ...");
    getchar();  
    return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容