Objective-C学习笔记(1-3)

去年因为项目需要临时学了一段时间Objective-C,可惜直到现在都适应不了这门语言。学习过程中根据不同主题记了一些东西下来,发出来共享,但愿能帮到一些同为入门菜鸟的Objective-C学习者,不过笔记中肯定有错,这个我比较有自知之明。笔记中的所有时间表达(昨天、这段时间)都不以现在为准,因为是好几个月之前写下的。

目录

  1. 构造和拷贝构造
  2. 动态特性和RTTI
  3. 内存管理和标准C区别

01构造和拷贝构造

首先是有关构造函数

我想实现子类基类不同数量参数的构造方式,结果昨天试验了几种方法都不行。

参见下面的代码,Person是基类,实现了一个两参数的构造函数initWithArgs。子类Coder重载了这个方法,多了一个参数,试图重用基类的方法:

// person的构造函数
- (id) initWithArgs : (NSString*) aName andAge:(int) aAge {
Person* person = [[Person alloc] init];
[person setName: aName];
[person setAge: aAge];
return person;
}
// coder的构造函数
- (id) initWithArgs : (NSString*) aName andAge:(NSInteger) aAge andLanguage:(NSString*)aLanguage {
return ???  //这里要返回什么
}

结果昨天尝试了N种方法,全都不行!

// 不行, crash!
if ( self = [super initWithArgs:aName andAge:aAge] ) {
[self setLanguage:aLanguage];
}
return self;

// 不行,没有crash,但是基类的参数name和age都设置失败
[super initWithArgs:aName andAge:aAge];
[self setLanguage:aLanguage];
return self;

// 好用,但我们的基类构造函数不就没用了吗
Coder* coder = [[Coder alloc] init];
[coder setName:aName];
[coder setAge:aAge];
[coder setLanguage:aLanguage];
return coder;

问题在于基类的构造定义,要用[self class]获取到当前self指针的类型才对

Person* person = [[[self class] alloc] init];

子类的构造函数:

Coder* coder = [super initWithArgs:aName andAge:aAge];
[coder setLanguage:aLanguage];
return coder;

或者用更单纯一点的调用:

[self initWithArgs:aName andAge:aAge];
    [self setLanguage:aLanguage];
    return self;

Read more >>>

《深度探索C++对象模型》学习笔记(5-7章)

第5章 构造析构和拷贝

虚函数

对于抽象类而言,是否应该自己负责初始化类内的数据成员?(别忘了抽象类也可以有非纯虚函数和数据成员)

  • 一般情况下被认为子类要负责从基类继承来的数据成员的初始化
  • 或者基类必须提供一个显式的构造函数用于自己数据成员的初始化
  • 最好的方法则是将行为和数据分离,提供接口专门用于定义方法
class A_B
{
public:
    virtual ~A_B() = 0;
    virtual void Say() const = 0;
    inline char const* GetData() const { return iData; }
protected:
    A_B( char* aData ) : iData( aData ) {  }
protected:
    char* iData;
};
inline A_B::~A_B() {}

class C_D : public A_B // concrete derived class
{
public:
    C_D( char* aData, char* aData2 ) :
           A_B( aData ), iData2( aData2 ) { cout<<"c_d ctor"<

纯虚函数

  • 拥有定义的纯虚函数(这还叫纯虚函数么)可以在子类中进行调用。不过就是什么用都没有罢了。
//定义
inline void A_B::Say() const { cout<<"hey!"; }
//调用,发现hey!并没有打印出来
virtual void Say() const { A_B::Say(); cout<<"c_d says: "<
  • 实际上是否让纯虚函数拥有(没用的)定义由程序员说了算。
  • 但唯一的例外是纯虚析构函数,必须像上面那样进行显式定义 A_B::~A_B() {}
    • 否则就会出现错误,严格来说并非编译错误,而是链接错误。
    • 原因很简单,析构函数和构造函数一样,子类的都会由编译器进行扩充,调用基类的版本。
    • 不希望这么定义的话,解决方法只有生命非纯虚的析构函数

Virtual Specification

  • 不加选择地将函数设定为virtual在效率上会不升反降,不能依赖编译器的优化
  • 应付const也会令人头疼,例如在基类中不需要修改的const ref或者const pointer,到了子类就需要修改了。目前最好的方法是不用const(……无语)
  • 需要考虑到的(不全)
    • 抽象类的构造函数需要负责初始化自己的数据成员,可以声明为protected避免外部访问
    • 缺少多态需求的函数不要设定为virtual,尤其是inline函数
    • 慎用const,除非确定派生类也不会修改const修饰的对象

Read more >>>

打自己脸——有关.net对象分配

之前发过的那篇.NET的GC机制和GC知识学习里面有一选项为:

A. 值类型的对象会被分配到Stack上,而引用类型的对象会被分配到Large Object Heap上。

今天在写前一篇Blog时候,发现这句话其实大错特错,虽然这个选项本身就是故意加入了错误,作为选择题答案出现,但此错非彼错。还好题目没有正式使用,不然会被人打脸打得啪啪响。在尚未被人打脸之前还是先自打比较好。先看前半句:

值类型的对象会被分配到Stack上

拿值类型中最常被提到用来和引用类型相比较的Struct来说,它一定分配到Stack上吗?当然不是。这一点题目已经涉及到了,就是大于85k的对象会分配到LOH上。但忽略的一点是一般尺寸的值类型对象也有可能分配到Heap上:

  • 类成员变量,就算是值类型也是在Heap上分配;
  • 局部值类型变量,涉及到和闭包相关的特性时。

第一条就不说了。第二条例如下面这个例子,i和j都是局部值类型变量,他们都在Stack上吗?

private void Form1_Load( object sender, EventArgs e ) {
    int j = 0;
    int i = 0;
    Func foo = () => {
        return i;
    };
}
当然是否定的,拿出il代码看一下就明了:从声明上来看,j的确是在Stack上没错,但i却是依附在一个编译器自动生成的类<>c__DisplayClass3上面;进行赋值时候i用的是stfld而不是stloc。这也就是说对于闭包涉及到的局部变量而言,CLR会将其依附到一个自动生成的类上进行存取操作,那么它自然就不分配到Stack上了。
.method private hidebysig instance void  Form1_Load(object sender,
class [mscorlib]System.EventArgs e) cil managed
{
// 代码大小       31 (0x1f)
.maxstack  3
.locals init ([0] int32 j,
[1] class [System.Core]System.Func`1 foo,
[2] class WindowsFormsApplication1.Form1/'<>c__DisplayClass3' 'CS$<>8__locals4')
IL_0000:  newobj     instance void WindowsFormsApplication1.Form1/'<>c__DisplayClass3'::.ctor()
IL_0005:  stloc.2
IL_0006:  nop
IL_0007:  ldc.i4.0
IL_0008:  stloc.0
IL_0009:  ldloc.2
IL_000a:  ldc.i4.0
IL_000b:  stfld      int32 WindowsFormsApplication1.Form1/'<>c__DisplayClass3'::i
IL_0010:  ldloc.2
IL_0011:  ldftn      instance int32 WindowsFormsApplication1.Form1/'<>c__DisplayClass3'::'
b__2'()
IL_0017:  newobj     instance void class [System.Core]System.Func`1::.ctor(object,
native int)
IL_001c:  stloc.1
IL_001d:  nop
IL_001e:  ret
} // end of method Form1::Form1_Load

另外漏了一点,在更正:值类型并不总是分配在栈上一文中提到,还有一种会被分配到Heap上的值类型:

  • 数组内的值类型对象,会被一同分配到Heap上。

一些关于闭包的废话

继函数形参传值传指针、GC机制的Blog后,貌似这又是说烂的问题?没办法,最近在回顾各种知识,发现有联系的就拿出来写一写,省得日后发霉。如果你现在对闭包还不是很了解,可以阅读一下学习Javascript闭包(Closure)深入理解JavaScript闭包(1)理解 JavaScript 闭包三篇文章。难度依次上升。

JavaScript的闭包

闭包的概念已经比科学发展观都深入人心了,简单说它就是“具有独立作用域并且可以引用外层作用域的对象”。例如C#中的匿名函数和Lambda表达式就是基于的闭包概念,其他的高级语言,类似Java、JavaScript和Python等也都有闭包的应用。当然也不要忘了JavaScript的内存泄露主因之一也是闭包,所以先看JS好了。

还是先从代码入手,下面两段代码是从阮一峰老师的那篇闭包讲解中摘取的最后习题,那么现在提问,两段代码分别alert什么内容?

var bar = "Global";
  var object1 = {
    bar : "local",
    foo : function(){
      return function(){
        return this.bar;
      };
    }
  };
  alert(object1.foo()());

//我是分隔线

  var object2 = {
    bar : "local",
    foo : function(){
          var that = this;
      return function(){
        return that.bar;
      };
    }
  };
  alert(object2.foo()());

Read more >>>

.NET的GC机制和GC知识学习

在刚刚完成的一套.net笔试题卷子中,出了这样一道题:

面有关垃圾收集(GC)机制的说法中正确的是:

  • A. 值类型的对象会被分配到Stack上,而引用类型的对象会被分配到Large Object Heap上。
  • B. 使用try/catch/finally在效率上并不优于使用using的方式。
  • C. 可以实现自定义引用类型的Finalize方法,但它不能被重载。
  • D. GC的垃圾回收动作可以在代码中手动调用。

答案是BCD,A中错误在于引用对象会被默认分配到GC Heap的SOH(Small Object Heap)上(update:错误不仅如此,请参考打自己脸——有关.net对象分配),超过85KB对象会被分配到LOH。至于85K这个Magic Number的由来,应该是编写.Net框架的工程师们经过平时各种使用场景和优化推算出来的,我们不必多费心研究。

那么GC Heap为什么要分两类呢?这要从SOH和LOH不同的垃圾清理机制说起。

.Net的GC机制

GC Heap

对于LOH来说,清理垃圾的过程等于“回收+归并”,即不仅要清理掉内存空间,还要将尚存的对象向着一个方向移动直到和之前的对象紧紧贴在一起[1][3]。如下所示,SOH上有四个对象:

[obj1][    obj2     ][  obj3  ][obj4]
↑
0代指针(p0)

经过一次清理后,obj2和obj4没了,留下来的都是1代对象,并将0代指针移动到内存低位的第一个空闲空间处:

[obj1][  obj3  ][obj5]
↑               ↑
p1              p0

2代对象的生成类似,这样的移动保证不会产生大量的内存碎片,最大程度提高了内存使用效率。

可惜对于大对象来说,移动的成本过高,所以会优先采用查找可用剩余碎片空间的方式,将可以容纳新对象的空间重复利用。类似上面的内存情况在0代清理、新分配了obj5后会变成:

[obj1][obj5]         [  obj3  ]

obj5和obj3之间的内存如果小于85K就很可惜,因为再也利用不起来了。

不过需要注意的是,虽然大对象目前不会被归并处理,但不保证未来的CLR版本会不会实现一个LOH也进行存留对象移动的版本,所以希望保持现在这种行为的话,需要在变量前加fixed关键字进行标识。

分代处理

上文提到的0、1代是.Net中采用的分代垃圾回收机制的体现。目前.Net的GC中支持的代数为2(GC.MaxGeneration),一般来说,可以将0-2这三代对象视作:

  • 0代,大多数对象,GC进行回收时候都会被处理掉
  • 2代,少数常驻内存的对象,例如asp.net和整个网站生存期等同的一些全局对象
  • 1代,介于0代和2代之间的一个灰色地带。

在GC动作的时候,0代会首先会被回收,如果需要的内存空间不够用再依次处理1和2代对象。

对于程序中的引用对象,可以通过GC.GetGeneration()获取它的代数,这个方法还有一个重载版本支持WeakReference。

Read more >>>