delphi D11编程语言手册 学习笔记(P139-P222)

 

这本书可以在 Delphi研习社②群 256456744 的群文件里找到. 书名: Delphi 11 Alexandria Edition.pdf

先置顶一下大佬的话

 

 

致看到这里的朋友一些提示: 书上的内容有些看不懂很正常,也不要着急,可以往下找找,作者可能在下面给出了答案,不要一有问题就到处找资料,问大佬,这样效率很慢!

 

●P139-143:

  1.数组类型的索引值不是非得从0开始,可以随意设置,只要是有序类型即可----你得确定你的代码只有你自己看.否则容易被人砍

  2.静态数组,不能再使用SetLength函数设置其长度,因为它是静态定长的  

type

TDayTemperatures = array [1..24] of Integer;

  3.数组的大小和边界

    3.1  使用Low High函数可以取得数组的下上限,可以减少对数组代码的维护成本,并使代码更可靠.

    3.2  使用Length函数可以获取到数组内,元素的个数.

  4.多维静态数组 

 

 正常情况下,一维数组里面都是存储着数据.而在多维数组里面,除了最后一维存了数据,其他维里存着的都是数组.所以多维数组的取值就是一层层的取,例如: arr[0] [0] [0] 或者arr[0,0,0]就能取出第三维中的第一个数据. 

静态数组的声明方式1:

var
MyArray:Variant;
MyArray:=VarArrayCreate([0,1,2,5],Varinteger)

这是一个二维数组,每一维都是成对出现,以构成起止索引,上面的代码中,第一维是从0到1,第二维是从2到5,数据类型不能是Integer,而是VarInteger;

静态数组的声明方式2(传统方式比较好理解):

type

TAllMonthTemps = array [1..24, 1..31] of Integer;  //注意中间的逗号
TAllYearTemps = array [1..24, 1..31, 1..12] of Integer;  //注意中间的逗号
我们可以这样来存取里面的元素:
var
AllMonth1: TAllMonthTemps;
AllYear1: TAllYearTemps;
begin

AllMonth1 [13, 30] := 55; // hour, day注意逗号 
AllYear1 [13, 30, 8] := 55; // hour, day, month注意逗号 

   静态数组会立即使用掉占用的空间,这些浪费是可以避免的.动态数组使用的是heap的记忆空间,所以我们用动态数组来避免这些浪费

typeTMonthTemps = array [1..31] of TDayTemperatures;  //第二维
TYearTemps = array [1..12] of TMonthTemps  //第一维,谁接在后面,谁就是小弟

存取数值的方式有两种,第一种是一层层的套 Month1 [
30][12] := 44;

第二种是用方括号括起来的坐标,中间用逗号隔开 Month1 [
30, 12] := 55; // day, hour Year1 [8, 30, 12] := 55; // month, day, hour

Delphi数组详解(整理)

●P144-152:

  1.动态数组在没设定长度(SetLength)之前,不能直接使用.SetLength函数会预设所有未赋值的元素值为0

  2.动态数组在有定义但无赋值行为之前,是无边界概念的,也就是说,即使你SetLength(arr,2),一样可以取arr[100]的值,其值按照上面说的预设值为0

  3.在动态数组使用 Low 函式,永远都会得到 0,使用 High 函式,则永远都会得到数组长度减一。也就是说,对一个长度为 0 的动态数组执行 High 函式,会得到-1

  4.使用Copy函数可以复制出数组的部分或者全部元素.

  5.使用 := 把一个数组赋值给另外一个数组,相当于把新增别名,两个数组指向的都是同一个内存空间.打个比喻就是张三取了个网名叫张飞,实体指向的都是同一个人.

  6.动态数组的常规运算:  

procedure TForm1.Button1Click(Sender: TObject);
var
  DI: array of Integer;    //还可以是其他类型
  I: Integer;
begin
  DI := [1, 2, 3]; // 初始化
  DI := DI + DI; // 追加元素 [1, 2, 3,1,2,3]
  DI := DI + [4, 5]; // [1, 2, 3,1,2,3,4,5]

  Insert([8, 9], DI, 4);    // 在索引4后面插入新元素[1, 2, 3,1,8,9,2,3,4,5]
  Delete(DI, 2, 1); // 删除索引2后面的1个元素[1,2,1,8,9,2,3,4,5]
  for I in DI do
  begin
    Memo1.Lines.Add(I.ToString);
  end;
end;

 7.开放数组.比较典型的例子是format函数的第二参数,它是一个不确定长度和不定数据类型的动态数组参数.

 要建立这样一个参数,常规情况下只要用const声明一个数组参数即可

function Sum (const A: array of Integer): Integer;
var
  I: Integer;
begin
  Result := 0;
  for I := Low(A) to High(A) do
    Result := Result + A[I];  
end;
//调用
X := Sum ([10, Y, 27*I]);
//也可以把整个数组传进去
var
  list:array of integer;
begin
  //list定义过程,略
x:=sum(list);

//也可以只传数组的一部分元素
X := Sum(Slice(list,5));

上面的例子中,数组参数是固定类型的integer,那如果我想传整数,又想传实数,甚至我还想传字符串等等类型怎么办?这就又还原到format函数第二参数中来了.以下是Format函数的源码:

function Format(const Format: string; const Args: array of const): string;
begin
  Result := System.SysUtils.Format(Format, Args, FormatSettings);
end;
//调用
str:=Format ('Int: %d, Float: %f',String: %s, [56, 12.4,'好好学习']) ;

注意看红色标识的参数声明,它会接受不确定数量的值.只要在Format定义范围内的都可以https://www.delphitop.com/html/hanshu/1883.html

开放数组参数与TVarRec类型的元素是完全兼容的,它们都包含了一个叫VType的属性,这个属性里面指明了开放数组参数的数据类型,以下是它支持的所有类型的列表

vtInteger   vtExtended   vtPChar
vtWideChar  vtCurrency   vtWideString
vtBoolean   vtString     vtObject
vtPWideChar vtVariant    vtInt64
vtChar      vtPointer    vtClass
vtAnsiString vtInterface vtUnicodeString

下面的例子把不同型别的数据做加总,把字符串转换成整数,字符转成对应的有序数值,把布尔值的 True 转成 1。这个源码可以说相当的高端(里面还用到了指针:pointers dereference),所以如果现在看不懂也不用担心:

function SumAll (const Args: array of const): Extended;
var
  I: Integer;
begin
  Result := 0;

  for I := Low(Args) to High (Args) do
    case Args [I].VType of
    vtInteger:
      Result := Result + Args [I].VInteger;
    vtBoolean:
      if Args [I].VBoolean then
        Result := Result + 1;
    vtExtended:

      Result := Result + Args [I].VExtended^;
    vtWideChar:
      Result := Result + Ord (Args [I].VWideChar);
    vtCurrency:

      Result := Result + Args [I].VCurrency^;
  end; // case
end;

//调用
var
  X: Extended;
  Y: Integer;
begin
  Y := 10;
  X := SumAll ([Y * Y, 'k', True, 10.34]);  //k =>107,True=>1
  Show ('SumAll: ' + X.ToString);  //218.34
end;

●P153-165

  记录类型,感觉有点像像其它语言里的对象{name: '张三',age: 18}

  1.数组以数字索引定义了一连串的元素,而记录则以名称定义对应的成群元素。换句话说,记录是一连串被赋予名称与对应字段的元素,每一组都元素都有其特定的资料型别。记录数据型别的定义会把这些字段全部列出,包含每个字段的名称,以及对应数据应有的类型。

type
TMyDate = record  //关键字,record
  Year: Integer;
  Month: Byte;
  Day: Byte;
end;
var
  BirthDay: TMyDate;
begin

  BirthDay.Year := 1997;

  BirthDay.Month := 2;

  BirthDay.Day := 14;

  Show ('Born in year ' + BirthDay.Year.ToString);
end;

  2.在上面的数组讲解里面,我们说过:

5.使用 := 把一个数组赋值给另外一个数组,相当于把新增别名,两个数组指向的都是同一个内存空间.打个比喻就是张三取了个网名叫张飞,实体指向的都是同一个人.

在记录类型里,如果变量指向另一个记录型变量,相当于是完全复制出了这个记录类型

var
  BirthDay: TMyDate;
  ADay: TMyDate;
begin
  BirthDay.Year := 1997;
  BirthDay.Month := 2;
  BirthDay.Day := 14;
  ADay := Birthday;//
  ADay.Year := 2008;
  Show (MyDateToString (BirthDay));
  Show (MyDateToString (ADay));
end; 输出结果(用国际日期格式):
1997.2.14 2008.2.14

 同样的,如果在函数参数中传值声明一个记录类型的参数,也是得到一份完全复制的记录类型.按照这个原理,如果你是想修改原记录的话,完全可以使用var传址声明这个记录类型

  3.在数组中使用记录 与 在记录中使用数组,更复杂一点的还可以相互套用

var
DatesList: array of TMyDate; I: Integer;
begin// 设置数组长度
  SetLength (DatesList, 5);
// 赋值for I := Low(DatesList) to High(DatesList) do begin
  DatesList[I].Year := 2000 + Random (50);
  DatesList[I].Month := 1 + Random (12);
  DatesList[I].Day := 1 + Random (27);
end;
// 调用
for I := Low(DatesList) to High(DatesList) do
  Show (I.ToString + ': ' + MyDateToString (DatesList[I]));

  4.可变记录(书中叫变异记录),不建议使用

  变异记录的应用已经大幅被面向对象或其他更新的技术所取代了,目前只剩很少的系统函式库在很特别的情境下会内部使用到这个技术了。

  5.字段对齐(看得我有点懞)

  Delphi - Record记录和变体记录

  这里涉及到了共用体,大佬说现在不用学

  

 

   6. With语句能使代码更精简,但同时也可能使一些微小的错误难以被找出,是一把双刃剑

with Record1 do
    Show (Name + ': ' + MyValue.ToString);

  来猜猜这个name是谁的name?  这是窗体的name,不是Record1 的name

  7.带有方法的记录

  一个带有方法的纪录,基本上已经跟类别非常接近了,他们仅仅是管理内存的方式上面,有所区别而已

ObjectPascal 的记录具备现代编程语言的两种基本功能:
  l 方法:也就是和记录数据结构直接链接的程序或函式,这些方法可以直接使用记录的数据字段。换句话说,方法就是在声明记录的时候同时声明的程序或函式(也可以只是提前声明)

  l 封装:透过封装,我们可以限制某些数据字段或方法不被其他源码直接使用。我们可以透过 private 这个存取描述字来提供封装的功能,好让其他源码无法看见位于 private 区段的数据或方法。而 public 区段的字段跟方法则可以被所有源码所使用。预设的存取描述字是 public    

type
TMyRecord = record
  private

     FName: string;
    FValue: Integer;
    FSomeChar: Char;

  public

    procedure Print;
  //提前声明方法
    procedure SetValue (NewString: string);//提前声明方法
    procedure Init (NewValue: Integer); 
//提前声明方法
end; 


public private 这些关键词在同一个记录声明中可以多次出现,但是不建议. 用下面的例子来说明public private 的作用.

在单元A中声明一个记录

 

 在单元B中uses A,然后你就会发现,private 下的内容是无法使用的,只能使用public下面的属性和方法.

 

   8.为记录初始化

  记录在声明时,不会自动初始化.像上面的例子中,R是没有经过初始化的,如果直接使用,虽然IDE不会报错,但结果肯定不是你想要的!

  要初始化记录变量有两种方法,一种是具现记录变量后,再一个字段一个字段的去赋值,一种是给记录设置默认值.

  

 

   如果你把  R.FAge := 20;  这句注释掉,恭喜你,你将会得到一个洪荒级张飞!这就是不初始化字段就使用的后果.

  

 

   为了避免这种情况发生,我们可以给记录设置默认值.

   1.使用 constructor Create 关键字来初始化函数.  

  TMyRecord = record
    FName: string;
    FAge: Integer;
    FSex: Char;
    constructor Create(AOwner: TComponent);
  //记录类型是不能使用destructor 构析函数销毁记录的.
procedure Print; end; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.Button1Click(Sender: TObject); var R: TMyRecord; begin R:= TMyRecord.Create(Self ); R.Print;      //直接使用,但得到的是默认值,不会错的太离谱 end; { TMyRecord } constructor TMyRecord.Create(AOwner: TComponent); begin FSex := ''; FName := '张飞'; FAge := 20; end; procedure TMyRecord.Print; begin ShowMessage(FName + ',' + FSex + ',今年' + FAge.ToString + '岁!'); end; end.

 ●P165-175

  重载运算符

  在我们正常的认知里面+-*/都是常规运算符,不会再有其它非分想法.但D / C++ 里面因为有重载功能,所以很自然的,这些运算符当然也能够重新定义

  关键字是 Class Operator

  语法是 Class Operator + 运算标识(参数): 记录类型; 这里要注意,运算标识是有固定值的,不能随便改.下表引用自万一的博客,其中 转换 类别不学太深究,平原君大佬说的:https://www.cnblogs.com/del/archive/2008/12/09/1351471.html

  

 类别 运算符运算符标识使用
转换 隐式转换  Implicit Implicit(a: type): resultType;
显式转换 Explicit Explicit(a: type): resultType;
一元 - Negative Negative(a: type): resultType;
+ Positive Positive(a: type): resultType;
Inc Inc Inc(a: type): resultType;
Dec Dec Dec(a: type): resultType
not LogicalNot LogicalNot(a: type): resultType;
not BitwiseNot BitwiseNot(a: type): resultType;
Trunc Trunc Trunc(a: type): resultType;
Round Round Round(a: type): resultType;
比较 = Equal Equal(a: type; b: type): Boolean;
<> NotEqual NotEqual(a: type; b: type): Boolean;
> GreaterThan GreaterThan(a: type; b: type) Boolean;
>= GreaterThanOrEqual  GreaterThanOrEqual(a: type; b: type): resultType; 
< LessThan LessThan(a: type; b: type): resultType;
<= LessThanOrEqual LessThanOrEqual(a: type; b: type): resultType;
二元 + Add Add(a: type; b: type): resultType;
- Subtract Subtract(a: type; b: type): resultType;
* Multiply Multiply(a: type; b: type): resultType;
/ Divide Divide(a: type; b: type): resultType;
div IntDivide IntDivide(a: type; b: type): resultType;
mod Modulus Modulus(a: type; b: type): resultType;
shl LeftShift LeftShift(a: type; b: type): resultType;
shr RightShift RightShift(a: type; b: type): resultType;
and LogicalAnd LogicalAnd(a: type; b: type): resultType;
or LogicalOr LogicalOr(a: type; b: type): resultType;
xor LogicalXor LogicalXor(a: type; b: type): resultType;
and BitwiseAnd BitwiseAnd(a: type; b: type): resultType;
or BitwiseOr BitwiseOr(a: type; b: type): resultType;
xor BitwiseXor BitwiseXor(a: type; b: type): resultType;

  怎么来理解呢,我打个比方,AB两个人(记录),有名字,有年龄,有性别.正常我们要比较他两谁的年龄大,是不是得这到写啊:

  IF A.age > B.Age then.. else...

  但是现在我要问的是:你两谁大?  虽然题面上没有说明是问年龄,但正常人还是能理解它的意思,但电脑不知道啊,你要是直接来个

  IF A> B then.. else...

  他直接报错给你看!所以我要私底下教电脑怎么做:

 看不下去了,P167-175,溜了溜了...

 ●P176-179

  变体类型( Variant ),书中叫变动型别,不建议使用这种类型.

  它包含了以下种类,使用VarType函数可以知道Variant变量的真正数据类型.

  

varAny
varEmpty
varSingle
varArray
varError
varSmallint
varBoolean
varInt64
varString
varByte
varInteger
varTypeMask
varByRef
varLongWord
varUInt64
varCurrency
varNull
varUnknown
varDate
varOleStr
varUString
varDispatch
varRecord
varVariant
varDouble
varShortInt
varWord



  1.变体类型可以接受各种数据类型而不报错(看上面的表就知道).

var
  V: Variant;
begin
  V := 10;

  V := 'Hello, World';
  V := 45.55;
end;

  2.变体类型在运行期间会尝试把数据转换成可以接受的类型,如果转换不了,就会抛出一个异常错误.所以使用了变体类型的源码运行都很慢.

const
MaxNo = 10_000_000; // 10 million
var
Time1, Time2: TDateTime;
N1, N2: Variant;
begin
Time1 := Now;
N1 := 0;
N2 := 0;
while N1 < MaxNo do
begin
N2 := N2 + N1;
Inc (N1);
end;
// We must use the result
Time2 := Now;

Show (N2);

Show ('Variants: ' + FormatDateTime ( 'ss.zzz', Time2-Time1) + ' seconds');

结果我就不贴出来了,Variant大概比Integer慢了近50倍(书上PC机的测试结果)

 ●P180-183:

  指针两三事

  指针强大而又危险?指针本身不存储数据,它只存储变量在内存中的地址,通过地址,我们可以找到变量的实际使用内存空间,进而得到里面的数据.

  指针是所有程序人员都应该知道的核发知识的一部分.

  指针的定义并不是通过关键字来来创建,而是使用特殊符号 (^ ,6上面的那个符号)来声明的

type
    TPointerToInt = ^Integer;
//我们定义好指针变量之后,就可以把另一个同型别的变量的地址指派进去了只要透过@符号即可:
var
P: ^Integer;
X: Integer;
begin

X := 10;

P := @X;
    //把指针指向X
P^ := 20;
    // P^可读可取
Show ('X: ' + X.ToString);
    //20
Show ('P^: ' + P^.ToString);
  //20
Show ('P: ' + Integer(P).ToHexString (8));  //0018FC18,16进制

全局的非指针类型,声明后自动分配内存,并初始化值

局部的非指针类型,声明后自动分配内存,不初始化值,值不确定(取决与别的程序对这块内存的操作)

全局的指针类型,声明后不自动分配内存,值为nil

局部的指针类型,声明后不自动分配内存,但会随机指向一个地址,所以地址不为nil

未被配置的内存空间进行存取操作,会导致内存违规存取而使程序错误(不同操作系统对内存违规存取的处理各有不同的作法,Windows 会跳出一个错误窗口,iOS Android 则是直接关闭该程序).

如果一个指针变量没有值,我们可以指派 nil 给它。我们可以先检查指标的内容是否为 nil,如果不是 nil,我们才能够去读取其指向的内存空间内容。
Object Pascal 提供了一个名为 Assigned 的函式,让我们可以方便进行这样的测试。

if Assigned (P) then
begin
  //如果P不为nil
end;

 

指针的内存空间需要用disposeof()来释放,没有把配置的内存释放掉的错误,通常称为内存泄漏(Memoryleak)
New Dispose 之外,我们也可以用 GetMem FreeMem 来取用额外的内存空间,这些作法都需要开发人员提供要配置的内存大小(编译器在使用New DisposeOf 的情境下,会自动配置所指定的空间)。在编译阶段还无法确定需要多大空间的时候,GetMem FreeMem 则会比较方便。

var
P: ^Integer;
begin
// 申请新的内存空间P
New (P);
// 赋值
P^ := 20;
Show (P^.ToString);
// 释放内存
DisposeOf (P);

 

应用程序可用的内存区分三类:全局变量区(存放全局变量)、栈(Stack)、堆(Heap)。应用程序开始时所有的全局变量的内存都被分配到全局变量区,局部变量分配到应用程序栈,应用程序结束时分配到栈中的变量内存会被栈管理器自动释放,堆上的变量内存必须手工释放。

堆(内存)
堆表示程序可用的内存区,也叫动态内存区。堆内存的分配与释放次序是随机的,这就是说,如果你按次序分配三块内存,那么到时并不按分配时的次序释放内存。 堆管理器会负责所有操作,你只需简单地使用GetMem 函数请求新内存或调用constructor 建立对象, Delphi 会返回一个新的内存块(随意重用已经丢弃的内存块)。

堆是应用程序可用的三种内存区之一, 其它两种分别是全局内存区(存放全程变量) 和栈。与堆相反,全程变量内存在程序启动时就分配,然后一直保留到程序终止才释放;栈的内容请详见术语表。

Delphi 使用堆为对象、字符串、动态数组及特殊的动态内存请求(GetMem)内存分配。

Windows 应用程序的地址空间最大允许有 2 GigaByte, 其中大部分能被堆使用。

栈(内存)
栈表示程序可用的内存区,栈内存动态分配,并按特定次序分配、释放。栈内存按后进先出次序(LIFO)分配,这表示最后分配的内存区先被释放。栈内存一般在例程中使用(过程、函数及方法调用)。 当你调用例程时,例程参数及返回值是放在栈中的(除非使用Delphi缺省调用方式,对调用过程进行优化)。此外,例程中声明的变量(在begin语句前的 var 块中)也存放在栈中,所以当例程终止时,这些变量会被自动清除(在返回调用点之前以LIFO次序释放)。

栈是应用程序可用的三种内存区之一,其它两种分别是全局内存区和堆。

Object Pascal 也定义了一个名为 Pointer 的数据型别,这个型别可以指向未定义型别的指针(就像 C 语言里面的 void*)如果我们使用了未定义型别的指标,我们就得用 GetMem,不能用 New 来配置内存空间。因为用 New 来配置,系统会以 New 所要配置的数据型别自动计算大小来配置,但为定义型别的指针无法自动由编译器判别需要多少空间。而我们使用 GetMem 来配置空间时,可以指定需要配置的内存空间大小。

 ● P184-197

  文件类型.这是一种原始老旧的把数据具现到实体文件的技术,现代D里面已经很少使用这种类型了,但这项功能一直都存在.和array,record类型一样,它是由 file 关键字创建的  

type

  IntFile = file of Integers;
var
  IntFile1: IntFile;

这里就不多介绍了,更多请参考:Delphi中的文件类型

 ● P185-222

  Unicode字节顺序标记

BOM 长度 字符集
00 00 FF FE
4 UTF-32, big-endian
FE FF 00 00 4 UTF-32, little-endian
FE FF 2 UTF-16, big-endian
FF FE 2 UTF-16, little-endian
EF BB BF 3 UTF8


BOM会出现在文档的最开始位置,后面紧接头Unicode数据,以一个内容为 AB UTF-8 文档为例,它的前五个 Bytes 会是这样的(按上表,前3 个是 BOM,后2 个是 AB 这两个字符):EF BB BF 41 42

如果没有BOM,则会被认为是ASCII文本文件.

if Char(NChar).IsControl then 

IsControl 其实是用来判断,这个字符是不是控制字符,而不是判断它是不是一个控件.我第一眼也以为是后者,结果闹了笑话.

字符类型也属于有序类型,我们也可以把 Ord, Inc, Dec, Hight,Low 这些函数套用在字符型别上面 .

 Unicode中使用upcase与LowerCase不一定有用,需要使用system.Character单元中的ToLower与ToUpper方法才行.

 

 

 system.Character单元里面的很多函数都很古老,有些甚至已经被弃用.

 

 

 

 注意char与chars方法的区别

char是把ascii码转成字符,比如char(80),它与ord函数互反ord('p').

chars是取字符串的第几个字符,比如str.chars[0] 等价于 char(str[1])

 ● P198

  字符串的数据类型

  string的内存结构: https://blog.csdn.net/aqtata/article/details/38905387

  1.像其他类型一样,string类型最好也要做好初始化工作,不然错都不知道错在哪里.

  2.setLength超过自身长度的字符串,会被填充垃圾值进去;反之则会截取字符串.

  3.字符串以#0标记结束.下图返回结果为 abOrder

  

 

 

 

  4."写入时才复制",当你把字符串赋值给另一个变量时

   Delphi 10.3.X 截取字符串函数subString 和copy()常用字串符处理函数用法

 使用var 与const传递的参数,不会使字符串的引用计数发生变动.

 上图地址不一致,下图因为str是VAR声明的,所以地址一致.

 

 ByteLength计算字符串内存长度.

integer()得到内存地址.

padLeft与padRight补位函数

str := '2b';
str := str.PadLeft(5, '0');//0002b
str := '2b';
str := str.padright(5, '0');//2b000

TEncoding被用于进行不同编码字符之间的转换

 

posted @ 2022-11-11 10:07  一曲轻扬  阅读(981)  评论(0)    收藏  举报