2019-05-23 16:14:14 Lin_52025 阅读数 595
  • C语言系列之 指针与数组复习

    尹成老师带你步入 C 语言的殿堂,讲课生动风趣、深入浅出,全套视频内容充实,整个教程以 C 语言为核心,完整精彩的演练了数据结构、算法、设计模式、数据库、大数据高并发检索、文件重定向、多线程同步、进程通讯、黑客劫持技术、网络安全、加密解密,以及各种精彩的小项目等,非常适合大家学习!

    22047 人正在学习 去看看 尹成

c++银行账户管理程序2

C++个人银行账户管理1的基础上进行以下改进:
(1)在类SavingsAccount中增加一个静态数据成员total,用来记录各个账户的总金额,并为其增加相应的静态成员函数getTotal用来对其进行访问。
(2)将类SavingsAccount中不需要改变对象状态的成员函数声明为常成员函数,比如getBalance等。
(3)增加日期类Date
整个程序分为5个文件:date.h account.h是类定义头文件,date.cpp account.cpp是类实现文件,main.cpp是主函数文件。

main.cpp

#include<iostream>
#include"date.h"
#include"account.h"
using namespace std;
double SavingsAccount::total = 0;
int main()
{
    SavingsAccount s0(1001,0.015,2008,11,1);
    SavingsAccount s1(1002,0.015,2008,11,1);
    Date d01(2008,11,5),d02(2008,12,5),d11(2008,11,25),d12(2008,12,20),d2(2009,1,1);
    s0.deposit(d01,5000);
    s1.deposit(d11,10000);
    s0.deposit(d02,5500);
    s1.withdraw(d12,4000);
    s0.settle(d2);
    s1.settle(d2);
    cout<<"-----------------------------------------------------------------------"<<endl;
    s0.show();
    s1.show();
    cout<<"各账户总额:"<<s0.getTotal()<<endl;
}

account.h

#ifndef ACCOUNT_H_
#define ACCOUNT_H_
#include"date.h"
class  SavingsAccount
{
    int  Id;  //帐号
    double  balance;  //余额
    double  Rate;   //年利率
    Date  lastDate; //上次变更余额的日期
    double  accumulation=0; //余额按日累加之和
    double static total;
    double  accumulate(Date date);   //获得到指定日期为止的存款金额按日累积值
public:
    SavingsAccount(int id, double rate,int year,int month,int day);  //构造函数
    void  deposit(Date date, double amount);  //存入现金,date为日期,amount为金额
    void  withdraw(Date date, double amount);  //取出现金
    void  settle(Date date); //结算利息,每年1月1日调用一次该函数
    void  show();   //输出账户信息
    int getId() {return Id;}
    double  getBalance () { return balance;}
    double  getRate() {return Rate;}
    double static getTotal();
};
#endif // ACCOUNT_H_INCLUDED
#endif // ACCOUNT_H_INCLUDED

account.cpp

#include<iostream>
#include "account.h"
using namespace std;
double SavingsAccount::accumulate(Date date)
{
    return balance*lastDate.distance(date);
}
SavingsAccount::SavingsAccount(int id,double rate,int year,int month,int day) :lastDate(year,month,day)
{
    Id=id;
    Rate=rate;
    balance=0;
    cout<<year<<"/"<<month<<"/"<<day<<"\t";
    cout<<"账户"<<Id<<"创建完成!"<<endl;
}
void  SavingsAccount::deposit(Date date, double amount)  ////存入现金,date为日期,amount为金额
{
    accumulation += accumulate(date);
    balance+=amount;
    lastDate = date;
    total += amount;
    cout<<Id<<"存入:"<<amount<<"!\t"<<Id<<" 余额:"<<balance<<endl;
}
void  SavingsAccount::withdraw(Date date, double amount) ////取出现金
{
    accumulation += accumulate(date);
    if(balance>=amount)
        balance-=amount;
    else
        cout<<"余额不足"<<endl;
    lastDate=date;
    total -= amount;
    cout<<Id<<"取出:"<<amount<<"!\t"<<Id<<" 余额:"<<balance<<endl;
}
void  SavingsAccount::settle(Date date)   //结算利息,每年1月1日调用一次该函数
{
    accumulation += accumulate(date);
    double settle = (accumulation/356)*Rate;
    cout<<Id<<"结算利息:"<<settle<<endl;
    deposit(date,settle);
}
void  SavingsAccount::show()    //输出账户信息
{
    cout<<"帐号:"<<Id<<endl<<"余额:"<<balance<<endl;
}
double SavingsAccount::getTotal()
{
    return total;
}

date.h

#ifndef DATE_H_INCLUDED
#define DATE_H_INCLUDED
class  Date
{
    int  year, month, day;
    int  totalDays;   //该日期是从公元元年1月1日开始的第几天
public:
    Date(int year, int month, int day);
    int  getYear() const { return year; }
    int  getMonth() const { return month; }
    int  getDay() const { return day; }
    void  show() const;   //输出当前日期
    bool  isLeapYear(int year) const; //判断当年是否为闰年
    int  distance(const Date& date) const;//计算当前日期与指定日期之间相差天数
};
#endif // DATE_H_INCLUDED

date.cpp

#include<iostream>
#include "date.h"
using namespace std;
Date::Date(int Year, int Month, int Day)
{
    year=Year;
    month=Month;
    day=Day;
    int Days=0;
    for(int i=0;i<year;i++)
    {
        if(isLeapYear(i)==true)
            Days=Days+366;
        else
            Days=Days+365;
    }
    switch(month){
    case 1:Days+=0;break;
    case 2:Days+=31;break;
    case 3:Days+=59;break;
    case 4:Days+=90;break;
    case 5:Days+=120;break;
    case 6:Days+=151;break;
    case 7:Days+=181;break;
    case 8:Days+=212;break;
    case 9:Days+=243;break;
    case 10:Days+=273;break;
    case 11:Days+=304;break;
    case 12:Days+=334;break;
    default:cout<<"month error!"<<endl;
    }
    totalDays=Days+day;
}
void  Date::show() const
{
    cout<<getYear()<<"/"<<getMonth()<<"/"<<getDay()<<endl;
} //输出当前日期
bool  Date::isLeapYear(int year) const
{
    if((year % 4 == 0 && year % 100 != 0) || year % 400 == 0)
        return true;
    else
        return false;
} //判断当年是否为闰年
int  Date::distance(const Date& date) const
{
    cout<<date.year<<"/"<<date.month<<"/"<<date.day<<"\t";
    return date.totalDays-totalDays;
}    //计算当前日期与指定日期之间相差天数

代码运行截图

在这里插入图片描述

更多见:

C++银行账户管理1
C++银行账户管理3
C++银行账户管理4

2019-07-10 21:07:37 weixin_44363885 阅读数 63
  • C语言系列之 指针与数组复习

    尹成老师带你步入 C 语言的殿堂,讲课生动风趣、深入浅出,全套视频内容充实,整个教程以 C 语言为核心,完整精彩的演练了数据结构、算法、设计模式、数据库、大数据高并发检索、文件重定向、多线程同步、进程通讯、黑客劫持技术、网络安全、加密解密,以及各种精彩的小项目等,非常适合大家学习!

    22047 人正在学习 去看看 尹成

Jacob的C++程序员光速入门C#系列:
C++程序员光速入门C#(一):总览、数据类型、运算符
C++程序员光速入门C#(二):流程控制、函数、类

之前看了一个很有价值的系列博客,博主以C++的视角来入门C#,个人感觉非常的棒。虽然是很久以前写的了,很多新特性都没有涉及,但作为入门参考学习还是很有价值的。这里我把它重新排版修订一下,作为读书笔记,也希望能帮助到读者。原博客地址: C++程序员快速学习C#---(一)

一.Hello world!

随着.NET的深入人心,作为一个程序员,当然不能在新技术面前停而止步,面对着c++在.net中的失败,虽然有一丝遗憾,但是我们应该认识到,对于c++其实就不应该对其在.Net中的表现有太大的奢望,因为毕竟它并不是一个.Net下的正统语言,.Net应该是c#的舞台,作为一个c++程序员,我们应该庆幸,因为我们学习c#其实是简单的直接的,需要我们接受的新知识其实不多。相对其他语言来说,我们应该可以更加平滑的过渡到c#的开发中.废话不多说,现在就让我们用c++的基础来学习这个渐渐壮大的新语言-----C#。

对于C#的讲解我只讲解和C++有区别的地方,相同的部分我就一带而过,这样的对比学习可以让我们在已有知识的前提下快速掌握C#。

一开始学习语言大部分的教程都会用一个Hello World程序来示范,我们也落入俗套,用Hello World来和C++中做一个比较。

/******************C++程序**********************/
#include <iostream> 
using namespace std; 
int main() 
{ 
    //C++程序 
    cout<<"Hello World!"<<endl; 
    return 0;
}


/********************C#程序*********************/
using System; 
namespace HelloWorld 
{ 
    class Class1 
    { 
        //C#程序 
        static void Main() 
        { 
            Console.WriteLine ("Hello World!"); 
        } 
    } 
} 

乍一眼看上去两者差不多,心中一阵窃喜,可以说C#对语法的定义更加严格一些。

首先对于程序的进入点,最大的区别就是Main函数的开头必须要大写。因为C#是一个完全面向对象的程序语言,所以它的所有代码都必须定义在一个类中,Main函数也不例外。同时因为.net程序在编译运行时都是先转为中间语言,然后中间语言再编译为机器语言,这样的好处有2个。一,如同Java一样,写好的程序可以在不同的系统中运行,而不需要改变程序;二,使用不同的语言写的程序,因为要转化为相同的中间语言,所以在程序开发中可以使用不同的程序语言编写,而相互调用。

当使用不同语言开发或者进行分类开发时,各自开发的程序中会出现相同的变量名,函数名等,所以在写C#程序时,必须把程序包涵在一个名字空间内。C++在多文件编程的时候出现重复的变量名的时候,会比较头疼,C#则没有这个问题,因为所有的类定义都要在一个命名空间里,而变量只能定义在类中,不存在所谓的全局变量。

定义名字空间使用关键字:namespace <空间名>,当一个命名空间中的代码需要使用在另一个名字空间中定义的名称,就必须包括对该命名空间的引用,使用点字符(.) 。

/********************C#程序*********************/
namespace HelloWorld
{
    class Program
    {
        static void Main(string[] args)
        {
            LevelOne.A a1 = new LevelOne.A();
            a1.a = 10086;
            LevelOne.LevelTwo.A a2 = new LevelOne.LevelTwo.A();
            a2.a = 10086;
        }
    }

    namespace LevelOne
    {
        class A
        {
            public int a;
        }
        namespace LevelTwo
        {
            class A
            {
                public int a;
            }
        }
    }
}

这里就定义了两个名字相同的变量,我们可以使用LevelOne.A 和 LevelOne.LevelTwo.A 来独立创建两个对象,它们互不干扰。建立了名字空间后,我们可以使用using关键字来简化对它们包含的名称的访问。和C++中使用using namespace std的含义相似。

对于主函数必须要有限定词static 这表明Main函数是静态的,在内存中只能有一个副本。

第一行中的using System.其命名空间是.NET应用程序的根名字空间,包涵了控制台应用程序所需要的所以基本功能。就如同C++中的头文件包涵在std这个名字空间中一样。

Console.WriteLine ("Hello World!"); 

Console是system名字空间中的一个类,其有一个WriteLine方法,它的作用和cout一样,输出一行字符串。

二.数据类型

C#中的数据类型和C++是类似的。

1.浮点类型

浮点类型中添加了一个精度更高的decimal类型,对于金融方面的程序开发,此种数据类型用来定义钱币.

2.bool类型

bool类型的变量只能赋值为false和true。虽然它们的含义仍然是0和非0,但是在使用中不能再给它们赋值成整数值,在判断语句中 if (bool a==1) 的使用都是错误的。这样的语法让bool类型的意义更加准确。

3.字符类型

char类型在C#中是16位的,它不能接收一个整数值,这与C++有所不同。

4.整数类型

整数类新中添加了byte(8位无符号整数),sbyte(8位有符号整数),short(16位有符号整数) 类型
long变成了真正的64位有符号整数,它可以用在64位机器的编程中。
uintushortulong顾名思义是没有符号的整数,它类似于C++中的unsigned int,名字换了一下而已。

long类型在C++中的大小是根据编译器位数改变的,32位编译器的时候是4字节,跟int没区别。而64位的时候才是8字节,坑爹!

5.字符串类型

string类型是字符串类型,它是引用的类型,它的使用方法和C++中string的使用相似,可以进行+运算(运算符重载)。
string类型有一些方法可以给我们使用,类似C++。例如:ToCharArray() 把字符串放入一个字符数组中等等,可以在MSDN中查找。

6.类型的转换

C#是一个强类型的语言,它的数值类型有一些可以进行隐式转换,其他的必须显式转换,隐式转换的类型只能是长度短的类型转换成长的类型,例如int可以转换成long,float,double,decimal。反之必须显式的转换。

int a=7;  
float b=a;  //隐式转换 
long c=12L;  //和C++一样必须加上后缀L才能将一个常量定义为long型
a=(int)c; //显式转换 

使用上面的显示转换不能用在bool和string类型上,如果希望string或者bool类型和整数类型之间的转化可以使用一个方法:

//Convert.To*****(val) 
//*****:一种数据类型(具体请参看MSDN)  val:可以是这种类型的变量 
int a=123; 
string str=Convert.ToString(a); 
bool m_bool=Convert.ToBoolean(a); 

7.枚举类型

C++和C#的枚举类型,定义相同,使用也相同,只要注意C#中语句最后不需要;结束符。同时定义枚举类型时也不许放在主函数代码段中它只能放在执行代码外面,如下

using System; 
namespace HelloWorld
{
    class Program
    {
        enum Week
        { monday=5, tuesday, wednesday,thursday, friday, saturday,sunday }

        static void Main(string[] args)
        {
            int a = (int)Week.tuesday;
            //输出数字6,Right?
            Console.WriteLine(a);
            Console.ReadKey();
        }
    }
}

8.指针类型

c++中奉为经典的指针类型,在C#中已经取消了,真不知道这个是一个好消息还是坏消息,不过在易用性方面来说因该是一个进步。不过c#中其实在隐藏了一个指针,我们会在后面说到,同时在C#中也可以包含不安全代码,这些代码就是使用了指针的代码。

9.结构类型

C#中的结构类型看上去和C++没有什么区别,定义使用也相似,但还有有很大的区别的,首先就和枚举类型相似, 最后不需要;结束符,同时定义时也不许放在主函数代码段中它只能放在执行代码外面。其二最大的区别就是C#中的结构已经和类相似了,不同的地方在于:C++中的结构中成员变量是公有的,而C#中是私有的,C#中的结构和类的区别唯一就是不能继承(但是可以有接口,这个以后会讲到),但是C#结构是在堆栈中创建的空间,所以最好是对小量的数据进行操作。

/**********************C#程序************************/
using System; 
namespace HelloWorld
{
    class Program
    {

        public struct Student
        {
            public int intVar;
            public double doubVar;
        }

        static void Main(string[] args)
        {
            Student a, b;
            a.intVar = 1;
            a.doubVar = 1.1;
            b = a;
            a.doubVar = 2.6;
            Console.WriteLine("{0}  {1}   {2}   {3}", b.doubVar, b.intVar, a.intVar, a.doubVar);
            Console.WriteLine("{1}  {0}   {3}   {2}", b.doubVar, b.intVar, a.intVar, a.doubVar);
        }
    }
}

/*********************************
输出结果: 
        1.1  1  1  2.6 
        1  1.1  2.6  1 
**********************************/

/*******************************
    //C#中的结构类型不能继承!
    struct A
    { }

    //ERROR!!
    struct B:A
    {
    }

    class C
    { }

    //YES!
    class D : C
    {

    }
********************************/

C#中的输出定位格式和C语言中的printf类似,但更加简洁,不需要在对不同类型的变量使用不同的占位符,只需对应后面跟着的变量,给出序号就可以了。

10.数组类型

数组的定义和C++有区别,看上去很别扭,定义语法为:

<类型>[] <变量名>  例:  int[] num; 

这样就定义了一个int类型的数组,但是切记它可和C++不同,[]里面可不要写内容哦!int[10] num可是错误的。确定数组的大小有两个办法:

一,在定义时指定数据

int[] num = {5,3,7,3};  

二,使用关键字new,例:

int[] num = new int[4];

当然两者也可以合起来,例:

int[] num = new int[4]{5,3,7,3};

注意:前面定义了4个数据,后面花括号里面就必须有4个数据,不然就是错误的。

//错误!!
//int [] num = new int[4]{4,3}; 

并且,对多维数组的定义和C++是不同的。C++中定义为

int num[3][4] = {1,2,3,4,5,3,2,3,4,2,3,4}; 

C#中定义为

int[,] num = new int[3,4]{{1,2,3,4},{5,3,2,3},{4,2,3,4}};

注意,和C++不同C#不能在数据列表中不分类,也就是说不使用{}把一组括起来是错误的,而在C++中是正确的。

//ERROR to C#!!!
//int[,] num=new int[3,4]{1,2,3,4,5,3,2,3,4,2,3,4};

对数据的使用和赋值也相应的变为

//C#的数组成员使用和赋值
num[2,1]=3; 
//C++的数组成员使用和赋值
num[2][1]=3;

C#在数组中最富革命性的改变,应该就是是添加了锯齿形数组(或者说交错数组)。例如它可以添加一组{{1,2,3,4},{2,3},{2,3,1}}长度不一样的数据,,在C++中只能创建一个三行四列的数组,在C#中它能够产生这样一个锯齿形数组,第一组中有4个数据,第二组中有2个数组,第三组中有3个,这样可以不浪费内存。

灵魂绘图

 

锯齿数组的定义和前面的定义也有区别,它更像是一个数组中包含了一个数组

int[][] num = new int[3][]; 
num[0] = new int[4]{1,2,3,4}; 
num[1] = new int[2]{2,3}; 
num[2] = new int[3]{2,3,1}; 

下面是上面的定义的一种简洁写法:

int[][] sum = {new int[]{1,2,3,4}, new int[]{2,3}, new int[]{2,3,1}}; 

三.变量

变量的使用和作用域和C++类似,没有什么特别需要注意的。但是首先我们应该看到,C#是一门完全面向对象的语言,也就是说定义的变量都变成了类的私有成员(定义时如果没加访问修饰符的话)。如果要在别的类中使用变量就需要在定义语句前加上访问修饰符public。

在C#中必须给每个变量添加访问修饰符

public int a; 
public int b; 

C#中的访问修饰符还有一些,如下:

internal:变量只能在当前程序中使用.
new:在用作修饰符时,new 关键字可以显式隐藏从基类继承的成员。
private:私有的,和C++中含义一样
protected:保护类型,和C++中含义一样
static:静态的,和C++中含义一样
readonly: 只读,在变量初始化(构造函数)以后就不许改变.
protected internal:双重限定,但只有这一个组合

访问修饰符new的用法如下

public class Program : BaseClass
{
   new public class Test//2、new修饰符 显式隐藏从基类继承的成员
   {
       public int x = 2;
       public int y = 20;
       public int z = 40;
   }

   static void Main(string[] args)
   {
       var c1 = new Test();//1、new操作符 创建对象和调用构造函数
       var c2 = new BaseClass.Test();
       Console.WriteLine(c1.x);//2
       Console.WriteLine(c2.y);//10
       Console.ReadKey();
   }
}

public class BaseClass
{
   public class Test
   {
       public int x = 0;
       public int y = 10;
   }
}

四.常量

C#有两种常量,一种是const修饰的,一种是readonly修饰的。

constreadonly的区别是:

1.readonly运行时常量,程序运行时进行赋值,赋值完成后便无法更改,因此也有人称其为只读变量。const编译时常量,程序编译时将对常量值进行解析,并将所有常量引用替换为相应值。

2.readonly常量只能声明为类字段(即C++中的成员变量),支持实例类型或静态类型,可以在声明的同时初始化或者在构造函数中进行初始化,初始化完成后便无法更改。const常量除了可以声明为类字段之外,还可以声明为方法中的局部常量,默认为静态类型(无需用static修饰,否则将导致编译错误),但必须在声明的同时完成初始化。

static void Main(string[] args)
{
    //OK
    const int a = 10;
    //ERROR!!
    //readonly int b = 10;
}

3.由于const常量在编译时将被替换为字面量,使得其取值类型受到了一定限制。const常量只能被赋予数字(整数、浮点数)、字符串以及枚举类型。而readonly则可以修饰类对象。

//ERROR!!
//public const DateTime D = DateTime.MinValue; 
//OK
public readonly DateTime D = DateTime.MinValue; 

五.运算符

C#中的运算符,优先级和C++一样,但是需要注意下面的四个运算符

*  ,  ->  ,  &   ,sizeof 

上面的四个运算符在C#的不安全代码中可以使用,但在一般的C#代码中使用是错误的,C#取消了指针,当然和指针有关的操作符都不能用了。

Reference:
C++程序员快速学习C#---(一)
C#高级编程 ——Christian Nagel & Jay Glynn & Morgan Skinner



 

2014-07-23 15:52:57 u013037201 阅读数 1416
  • C语言系列之 指针与数组复习

    尹成老师带你步入 C 语言的殿堂,讲课生动风趣、深入浅出,全套视频内容充实,整个教程以 C 语言为核心,完整精彩的演练了数据结构、算法、设计模式、数据库、大数据高并发检索、文件重定向、多线程同步、进程通讯、黑客劫持技术、网络安全、加密解密,以及各种精彩的小项目等,非常适合大家学习!

    22047 人正在学习 去看看 尹成

本书系统地介绍C++的语法规则和面向过程、面向对象的程序设计方法。内容包括:C++语言概述,运算符、表达式和语句,控制结构,数组、结构体和共用体,函数,指针,类和对象,类的继承,多态性,输入/输出和异常处理。

2018-09-29 21:09:51 yaojiawan 阅读数 4872
  • C语言系列之 指针与数组复习

    尹成老师带你步入 C 语言的殿堂,讲课生动风趣、深入浅出,全套视频内容充实,整个教程以 C 语言为核心,完整精彩的演练了数据结构、算法、设计模式、数据库、大数据高并发检索、文件重定向、多线程同步、进程通讯、黑客劫持技术、网络安全、加密解密,以及各种精彩的小项目等,非常适合大家学习!

    22047 人正在学习 去看看 尹成

     大多数嵌入式工程师使用C语言来编写Cortex-M系列MCU 的程序,大家总觉得C++是用来编写Windows 或者Linux 应用程序的。特别是硬件工程师,也许压根就没有使用C++来编写程序。

      当我们阅读Mbed OS 的代码时却发现,许多是使用C++来编写的。C++是一种功能强大的面向对象的程序设计语言,在嵌入式系统软件开发中使用C++,会获得意想不到的简洁和喜悦。

     事实上,arduino 的程序设计语言也是C/C++。在arduino中也可以设计C++的类。许多arduino的库都是C++的类。Arm keil 中的MDK-ARM V5.14 也支持C++程序设计语言。

但是身边的嵌入式工程师使用C++语言特性的并不多见。

 

C++ for embedded system

下面我们就从嵌入式程序员的角度来谈谈C++。

对C++语言的误解

对于嵌入式程序员来讲,对C++语言有一些误解:

  1.  C++太慢
  2. C++ 生成的目标代码过于庞大
  3. C++ 不是ROMable的
  4. C++ 库过于庞大
  5. 抽象造成了低效率
  6. 虚拟函数太慢

    所以这些观点都是错误的。据有关文献表明,C++的源代码可能比C++程序多一些,但是目标代码并不比C语言产生的代码大和慢。最多只有10%的差异。

C++的版本

    C++ 是一种面向对象的程序设计语言,它是1979年美国贝尔实验室在C语言的基础上发展起来的。国家标准化组织对C++建立了标准化,之前稳定的版本是C++98版本。 C++的版本为C++11 是ISO 2011年的标准版本,C++14 是2014年的标准版本,C++17 是2017年的标准版本。

C++是C语言的一个超集,所以在C++编译环境下,我们依然可以使用C编写程序。

C++ 的特点

    C++和C最大的区别就是C++是面向对象的程序设计语言。而C是面向过程的程序设计语言。通俗地讲,C++是以数据为中心的,而C是以流程为中心的。学习C++ 程序设计的关键不是去熟悉C++的语法,而是要从以流程为中心转向对象为中心的思维模式。

C++最重要的概念就是类和对象。

        对象(Objects)是要程序处理的任何一个物体,我觉得可以翻译成物体,事物等等。面向对象,台湾翻译成 物件导向,我觉得更为贴切一点。对于嵌入式程序而言,软件更加接近物理物体。比如一个引脚,一个马达,一个SPI端口,甚至于一台CNC机床,一台注塑机。在C++ 程序中都被看做一个和多个对象。

   它是如何做到的呢?使用的最基本的方法就是简化和抽象。在面向对象的程序设计语言中,将对象抽象成了一组数据,以及对这些数据操作的函数,并称之为类(class)。

     形式上,就好比将几个函数加入到了结构类型(struct)中。在 C 中,结构是数据的凝聚,它将 数据捆绑在一起,使得我们可以将它们看作一个包。它们的处理可能在其它的地方。然而将函数也放在这个包内,结构就变成了新的创造物,它既能描述属性(就像 C中的 struct 能做的一样),又能描述行为,这就形成了对象的 概念。对象是一个独立的有约束的实体,有自己的记忆和活动。

为了加深印象,我们来举一些例子:

例如:我们来定义一个循环缓冲区类:

循环队列是嵌入式程序设计中常用的数据结构,例如在 串口UART通信中,如果使用终端方式就需要使用循环队列。它大概需要下面几个数据结构和操作:

  1.  缓冲区buffer
  2.  输入指针 input_index;
  3.  输出指针 output_index;
  4. 计数器  counter

操作程序包括

  • init()
  • put(char v)
  • char get()

C语言的代码为:

uint8_t buffer[SIZE];
int input_index;
int output_index;
int counter
void init()
{
input_index=0;
output_index=0;
counter=0;
}
void put(uint8_t value)
{
buffer[input_index++]=value;
if (input_index==SIZE-1) input_inedx=0;
counter++;
}
uint8_t  get(){
int temp;
temp=buffer[output_index++];
if (output_index==(SIZE-1))
   output_index=0;
counter--;
}

 如果使用C++来编写一个循环队列的类:

class circular_buffer {
public:
 circular_buffer();
put(uint8_t val);
 uint8_t get();
private:
 int input_index;
 int output_index;
 int buffer[SIZE];

}
circular_buffer:: circular_buffer()
{
  input_index=0;
  output_index=0;
  counter=0;

}
void  circular_buffer:: put(uint8_t value)
{
buffer[input_index++]=value;
if (input_index==SIZE-1) input_inedx=0;
counter++;
}
uint8_t  circular_buffer:: get(){
int temp;
temp=buffer[output_index++];
if (output_index==(SIZE-1))
   output_index=0;
counter--;
}

    

对象是类的实例,就好比变量是类型的实例。上面的类实例化

circular_buffer mybuffer;

   从形式上看,没有太多的变化,实际上,我们使用Class 描述的循环队列,不那么凌乱。更像是一个硬件的元器件。面向对象程序设计是我们用更接近物理特性的方式来描述物件。用类描述的循环缓冲区更像一个硬件器件。这才是OOP 的独特魅力。

  如果我们在程序中使用多个循环缓冲区的话,使用类(CLASS) 就显得简洁了,只要写成:

circular_buffer inputBuffer;
circular_buffer outputBuffer;

通过C++ 的类,我们将循环缓冲区变成了像一个元器件,我们不在需要关心内部的实现细节,而只需要关心它给使用者呈现出来的性能就可以了。我们也可以使用一个符号来表示。 

circular buffer

 

我们有必要再去学习C++呢?我也疑惑过,以至于很长时间没有去关心Arm公司C++编译器。直到Mbed OS 中大量地使用C++来编写程序,才开始认真地开始研究C++在嵌入式程序设计中的应用。

从C语言转向C++,不仅仅是你要学习新的语言规则,而是要改变编程的思维方式。最有价值的可能不是那些已存在的代码库(给出合适的工具,可以转变它),而是已存在的头脑库。一旦从C语言面向过程的方式转变为面向对象程序设计方式。你的效率会大大提高。所以C程序员学习C++的一项值得的投资。程序员是在用问题空间的术语描述问题的解(例如“把锁链放在箱子里”),而不是用计算机的术语,也就 是解空间的术语,描述问题的解(例如“设置芯片的一位即合上继电器”)。程序员所涉及的是 较高层的概念,一行代码能做更多的事情。

      在MBed OS 中,硬件接口都是使用类来定义。使程序员不需要关注细节,就可以直接访问和控制硬件电路。例如下面这些都是接口类。

DigitalOut 数字输出类

构造函数  

DigitalOut myled(PF_14); 它真正指定了STM32F429 的一个GPIO 脚

成员函数

write (int value);

read ();;

使用方式:

 myled.write(1);
   myled=1;
   myled=!myled;

 看到了吧?使用C++定义的硬件类,使我们控制GPIO 变得非常的方便。

      使用C++ 的类,和类似硬件设计那样,利用简单的功能电路构造构建了一个功能更强大,更智能的硬件零部件。在modular-2 电脑中,所有的I/O 接口板都对应封装了一个接口类(interface Class).将硬件抽象成了软件的类。

IOmoduleClass

     应用程序不再需要了解IO 模块的硬件细节,只要了解接口类就好了。正是由于Mbed OS 使用了C++类来设计API。才成就了Mbed OS 简单易学的特色。

C++ 的类是软件模块化设计的基石。使用类,可以几乎描述所以的物理部件的特性,比如步进电机,ADC 等等。

现在,我爱使用C++ 编写嵌入式程序,这个过程和使用各种元器件搭建PCB板,然后构成一个设备非常相似。编写一个好的类,就好像设计了一个芯片。既可以自己使用,也可以给朋友分享。

 

 

2010-06-07 21:56:00 ZhangYafengCPP 阅读数 4394
  • C语言系列之 指针与数组复习

    尹成老师带你步入 C 语言的殿堂,讲课生动风趣、深入浅出,全套视频内容充实,整个教程以 C 语言为核心,完整精彩的演练了数据结构、算法、设计模式、数据库、大数据高并发检索、文件重定向、多线程同步、进程通讯、黑客劫持技术、网络安全、加密解密,以及各种精彩的小项目等,非常适合大家学习!

    22047 人正在学习 去看看 尹成

                                                                                                               用汇编分析C++程序

 

     O  、引言

C++也有很长一段时间了,也写过一些程序。但学着学着,总被她强大的语法等等搞晕。以前用起来从不犹豫的东西,用着用着有时候就感到非常不确定。如果能看看编译器所对应生成的汇编代码,就会对此有深入的理解。让我们通过对C++程序对应生成汇编的分析,来了解C++的语法。也可知她与C的异同。

 

我们想通过对C++程序得到汇编代码,可通过编译得到。介绍几种常用编译器的获取汇编码的方法,比如我们的源文件是 Test.cpp

1.     Microsoft Visual C++cl /FAs Test.cpp

也可以通过加上优化选项 /O1 /O2等得到优化后的汇编代码。

2.     Borland C++                bcc32 –S Test.cpp

3.     Gcc/G++:                        g++ -o AssemFile.asm -S Test.cpp

 

对于前两种,默认在当前目录下,获得与源文件相同名,以.asm为后缀的MASM汇编文件。对于G++,得到的文件名可–o 选项后所跟的文件名相同的汇编文件。

 

当然,你的编译器是安装好的。Borland C++ MSVC++生成的汇编文件大致兼容,但G++生成的PowerPC汇编代码与前两种生成的不同。本例中我们用MSVC++所生成的汇编代码来分析。

 

如果你的MSVC++环境变量没有设置好,我们写一个BAT文件:

 

  

因为我的MSVC++9.0根目录在D:/Program Files/Microsoft Visual Studio 9.0/VC下,所以你的VC9DIR可根据你的实际情况更改之。在命令行下运行这个BAT,然后可以用MSVC++的命令行模式了。一切准备就绪,我们切入正题。

 

一、   引用

“引用”是C++中引入的重要概念之一。指针的不安全使人们对它诟病颇多,C++引用机制恰如其分的解决了这一问题。在C++语法上,引用和指针是不同的,但在内部实现机制上,它们是完全相同的——所以,引用和指针在机器码层次没有效率高下。

 

来个例子,先分析之。我们有一个Test.cpp文件如下:

 

 

 

我们编译之:cl /FAs Test.cpp。便在当前目录下获得 Test.asm 汇编文件。打开之,如下:

 

           

        

    为了容易理解,我将部分稍微改动了下。但有详尽的注释,相信很容易理解。我们看到,程序将局部自动变量分配在栈中。而将常量字面值字符串放入CONST段。代码很显然在_TEXT段。在使用外部函数时,需用exrern导入。就像printf——为什么我们不用std::cout实现输入呢?因为这样生成的汇编文件确实有点大,因为会导入很多的库函数,可能只这些导入代码就得200多行!为了理解简便,我们用printf替代之——反正效果一样!

 

        我们稍微分析下,就可以看到iValpIntiRef所占空间都是4字节。很容易理解,因为我的机子系统是32WinXP。其余不重要,唯有27~42行是重点。对照注释,我们发现,对pInt指针和iRef引用的实现代码一模一样!都将它们用iVal的地址初始化:

        

而在使用它们时,采取措施也一模一样!都是先取得其所存放地址值,然后通过这个地址取得相对应DWORD双字int值!由此看见:在内部实现上,指针和引用完全一样!

 

        既然指针和引用实现机制完全一样,为什么在C++中还要引入“引用”的概念呢?引用是对程序员来说的,我们可以通过引用,写出更安全、更健壮的程序来。我们接着看:

 

二、   结构

我们接着看结构在内存中的存放方式和其使用。依然看程序:

 

呃,很简单的程序。但可以反映出很多的知识来!依然cl /FAs Test.cpp,我们挑重要的代码段来看:

 

1.      Main函数对应汇编代码:

 

        

        

        有详尽的注释,看懂应该不难。可见,在调用函数f之前,分配了两个临时变量用以复制传递函数返回值!因为在函数调用时,栈的某处已经存放的是临时变量temp1的指针(6970),在函数f中,最后返回时,将Re的值全部复制到temp1中——当然是通过这个指针来作为桥梁实现的。

 

2.      f函数对应的汇编代码:

 

 

 

_TEXT代码段中,我们可以发现这样的函数。首先用public声明函数f,然后为局部变量Re分配12字节空间。等等!!我们的结构中只有2int4字节,ch1字节,总共9字节。为什么给Re分配了12字节?!!这就是编译器的高深之处了。数据结构中的数据变量都是按定义的先后顺序来排放的,第一个数据变量的起始地址就是数据结构的起始地址,结构体的成员变量要对齐排放。所以编译器自动帮你实现了内存对齐——为ch扩展了3个字节的对齐空间。保证它们都对齐在以4为边界的内存处。

 

        49行,函数返回时,首先将临时变量指针存入eax所以在main函数那张图上对应74行之下代码都可以顺利执行。通过此例看出,对一个结构的返回,来来回回需复制3次!——这是不优化的情况。其实对于这种情况,再优化也得复制2次!!对于这样的小结构来说还不算什么,但对庞大的结构来说,就必须考虑其开销了——不管是空间还是时间开销!这也是为什么鼓励通过指针或引用来传递返回值获传递参数的原因了。

 

        注意:对于稍大的结构来说,通过传递指针或引用来调用参数是肯定没错的。但对像intfloatchar等等单个变量,用引用或指针来传递参数或返回值,是得不偿失的!因为只需用一个寄存器便可完成传递,而不必再执行开辟局部变量、临时返回变量空间,对指针进行操作等等繁杂的步骤了。

 

三、   

接着我们看类的实现。依然是例子。

  

 

代码依然很简单。我们分别用structclass声明了两个类:_A _B。有什么区别吗?_A是用struct声明的,成员函数f是非虚的;_B是用class声明的,成员函数f是虚函数,除此无他。接下来的事情变得简单有趣:依然 cl /FAs Test.cpp。获得汇编文件Test.asm。打开之,呃,汇编代码有些长。

 

我们曾经在书本上这样学:“structclass都可以声明类,除了struct的成员默认是public的,class声明的成员默认是private的之外,并无区别。”这句话非常对。我们看其汇编代码(有些地方我为了理解方便,把那些诸如@??_C@_06PHGHDMGF@?$CFd?0?5?$CFd?$AA”吓人的字符串改为相对应有意义的字符串了):

 

1.      Main函数:

 

         

 

    依然_TEXT代码段中。开始处有三个public函数声明。通过详尽的注释,可以轻易看出它们分别是类_A_B的成员函数及构造函数——但是我们并没有给_B声明构造函数啊!我们已经知道,类如果没有构造函数,则编译器会为它自动生成一个无参数构造函数。但为什么_B自动生成了,而_A却没有呢?噢!_A中的f函数不是虚函数(这与是用struct还是class声明类无关)。

 

        然后为局部自动变量ab分配空间。a8字节,b12字节。等等,_A_B的数据成员一模一样,我们知道,成员函数是不占用类对象存放空间的,那为什么b的空间比a多呢?显然这儿不是为内存对齐而准备的!仔细一瞧,_Bf函数有virtual关键字——这就决定了_B声明的对象都会自带一个虚函数表指针(VfTable),我们知道指针占4字节,所以b对象就比a对象多了4字节空间。

 

        通过注释C++代码看出,然后构造了两个变量。通过汇编看出,a对象没有任何操作——因为它没有缺省构造函数,编译器也不会为其生成。而通过我给出的汇编注释可看出,对b对象的构造,先将b的地址放入ecx,然后调用其构造函数。我们只需记住:“在调用对象的成员函数之前,必先将对象指针放入ecx,然后紧接着调用函数!”在成员函数中,ecx中的指针就被当做this指针。简单而深刻!

 

        接下来的代码简单易懂——分别为ab成员赋值。然后分别调用ab的成员函数f——首先将对象地址放入ecx,接着调用。Main函数很是简单。我们接着一一解析其余三个函数。

 

2.      _B::_B:编译器自动生成的构造函数

 

 

 

先为this指针在栈中分配4字节空间。将ecx中的this指针保存到栈中。然后再184行设置其虚函数表。这个__B@@VfTable是什么呢?稍前我们可以看到:

 

                

 

可以看到,在虚函数表中只有169的一项:__B@@f即是_B的成员函数f了。其实,除了虚函数

表外,还有其他的东西。比如RTTI运行时信息、类继承等等信息。你可以自己看下。通过这儿,

我们理解了为什么编译器非要给没有构造函数的类生成构造函数的原因——因为它要进行虚函

数表、设置RTTI信息等等一系列操作!

 

3.      _A::f函数

 

                

       

 通过注释,很容易理解。76行的szPrintInfo就是字符串“%d, %d”。其余均无难度。

 

4.      _B::f函数

 

         

        

_A::f的实现并无多大区别。

 

        通过上面一系列犀利的操作,我们可以得出下面结论:

1>    类的数据成员在内存中的存放是连续的,存放次序与类体中的声明次序一模一样。

2>    类的数据成员如果没有对齐,有的编译器会为之自动对齐——注意,这儿是“有的编译器”!

3>    如果类没有任何构造函数,且它有虚函数时,编译器必须为之自动生成一个默认无参数构造函数。否则,不进行任何自动生成操作。

4>    类的构造函数(无论是自己写的还是编译器自动生成的)中,都会先设置虚函数表——如果此类有虚函数的话。

5>    类的所有成员函数(包括构造与析构),都不占用对象的存储空间——因为它们都是在外部的。

6>    如果一个类有虚函数(可以是多个),则其声明的对象在开头4字节有一个虚函数表指针(VfTable)。当类没有虚函数时,并不会有虚函数表产生。

7>    类对象在调用成员函数(包括构造与析构)时,必先将对象地址存放到ecx中,紧接着(注意这三个字)调用其成员函数。

8>    在类的成员函数中,函数通过传递来的存放于ecx中的this指针来引用数据成员。

 

我的总结就是这些。你可以通过查看继承、模板、RTTI、多态等等C++机制的汇编代码可以获得其实现机制——强烈建议你这么做,这样,我们学到的不只是怎样用这门优雅的语言,而更会了解其本质!

 

四、   小结

 

整整一个下午,终于将这篇帖子写完了。近来看《编程卓越之道——运用底层语言思想编写高级语言代码》,看不到几页,便顿足长叹——不仅是为其中各种语言的神奇玄妙而感叹,更为作者敏捷的才思、无处不在的智慧所赞叹!不管你用不用汇编,不管你用不用C++,不管你是否想了解语言的本质,我还是强烈建议你看看这本书,它带来的不仅是对高级语言的汇编底层解释,更是展示给了我们更深、更玄奥、更神奇的东西!!

 

我们在写代码的时候,如果不了解其最终生成的东西是怎样的,便不会写出更好的代码来。我非常庆幸自己遇到了这本书,它彻底解决了我以前的种种疑惑,更教会了我怎样通过挖掘高级语言代码的底层实现,来发掘高级语言中隐藏在深处的宝藏!!!

 

就这样。如果还有什么疑惑,非常愿意和你交流。我的联系方式:

                                                                                   

 


                                                                                                                                                --Time: 2010.6.7

                                                                                                                                                --Email:zyfgood12@163.com

                                                                                                                                                --Blog:http://blog.csdn.net/ZhangYafengCPP

【转载请注明出处】

没有更多推荐了,返回首页