Array
Array ist eine gängige Datenstruktur in der Entwicklung. Natürlich wissen wir, dass Array einem Container entspricht, aber es kann nicht nur Werte und Zeichen speichern, sondern auch Speichern Sie den Eintrag der Funktionen, die Adresse und die Daten der Struktur.
typedef struct _value { int val_1; int val_2; }VALUE; typedef struct _table { char index; char data[100]; int (*UniSetFunc)(VALUE*); }TABLE; int add(VALUE *val ) { int temp = 0; temp = val->val_1 + val->val_2; return temp; } TABLE page[10] { {“ First section ”, “abcdefghijklmn”, add}, {“Second section”, “opqrstuvwxyz”, NULL} }; int main() { VALUE AddValue; AddValue.val_1 = 2; AddValue.val_2 = 4; int result = 0; result = page[0]-> UniSetFunc(&AddValue); printf(“The Result of add is %d\n”, result); return 0; }
Zu diesem Zeitpunkt wird das Array in eine Struktur umgewandelt, die dem Wörterbuch in der Python-Sprache ähnelt, was die spätere Entwicklung und Nutzung sowie zusätzliche Upgrades und Wartung erleichtert.
Code-Analyse: Zunächst wissen wir, dass der Name der Funktion als Eingabeadresse der Funktion verwendet werden kann (ähnlich dem Namen eines Arrays, der die Adresse des Arrays darstellt), also in der In der TABLE-Struktur definieren wir eine Member-Funktion int (* UniSetFunc)(VALUE*); Hier dient UniSetFunc als Eingabeparameter der Funktion und VALUE* stellt den formalen Parametertyp der Funktion dar, die Seite [10] enthält Daten der Struktur und die Eintragsadresse der Funktion, die über Seite [0]-> UniSetFunc(&AddValue) aufgerufen werden können, um die Add-Funktion indirekt aufzurufen und die Summe der beiden Zahlen AddValue.val_1 und AddValue.val_1 zu implementieren Wert hinzufügen.
Problem mit der Speichertrefferrate:
Um die Effizienz der Codeoperation zu verbessern, muss der Speicherplatz vollständig genutzt werden und auf den Speicherbereich sollte kontinuierlich zugegriffen werden.
Wir wissen, dass der Speicherort des Arrays ein ganzer Bereich ist und kontinuierlich gespeichert wird. Für das definierte Array array[2][2] wird die Adresse array[0][0] angenommen von ist 0x04030, dann sind die Adressen von Array[0][1], Array[1][0], Array[1][1] jeweils 0x04031, 0x04032, 0x04033; So viel wie möglich Kontinuierlicher Zugriff auf Speicherbereiche statt Sprungzugriff. Schauen wir uns als nächstes ein Matrixmultiplikationsproblem an.
for (int i = 0; i < 2; ++i) { for (int j = 0; j < 2; ++j) { for (int k = 0; k < 3; ++k) { matrix[i][j] += matrix1[i][k] * matrix2[k][j]; } } }
Der obige Code ist eine häufig verwendete Methode zum Multiplizieren der Matrixmatrix1 und Matrix2 und zum anschließenden Zuweisen zur Matrix. Das heißt, die Matrix1-Matrix wird verwendet, um den Zeilenvektor mit dem Spaltenvektor der Matrixmatrix2 zu multiplizieren. und dann der Matrix zuweisen, also weil In Bezug auf die Struktur der Matrixspeicherung im Speicher können wir klar erkennen, dass beim Zugriff auf Matrix2 kein kontinuierlicher Zugriff verwendet wird, sodass die Speichertrefferrate niedrig ist. Als nächstes betrachten wir eine Methode mit hoher Speichertrefferrate.
for (int i = 0; i < 2; ++i) { for (int k = 0; k < 3; ++k) { for (int j = 0; j < 2; ++j) { matrix[i][j] += matrix1[i][k] * matrix2[k][j]; } } }
Es ist ersichtlich, dass der Code nur die Positionen der zweiten for-Schleife und der dritten for-Schleife austauscht, während sich die anderen Teile nicht geändert haben. Die Speichertrefferrate wurde jedoch erheblich verbessert. Der Zweck der Matrixmultiplikation besteht darin, die internen Elemente von Matrix1 und Matrix2 nacheinander zu multiplizieren und dann zu akkumulieren. Auf diese Weise tritt beim Zugriff auf die Matrizen Matrix1 und Matrix2 kein Speicherfehler auf, wodurch die Wahrscheinlichkeit von Speichertreffern erhöht wird.
Die Beziehung zwischen volatile, const und static:
Das Schlüsselwort const ist ein konstantes Schlüsselwort, und der Betrag, auf den es einwirkt, ist eine Konstante. Das Programm darf den Wert der Konstante nicht ändern, z. B. const int value = 12; Konstante darf vom Programm nicht geändert werden. Das Schlüsselwort const wird häufig während des Entwicklungsprozesses verwendet. Um zu verhindern, dass das Programm versehentlich eine feste Konstante ändert, sollten wir ihr zusätzlich das Schlüsselwort const hinzufügen. Wenn das Schlüsselwort const auf eine Konstante wirkt, muss die Konstante direkt initialisiert werden, da sie nicht geändert werden darf, während das gesamte Programm ausgeführt wird. Beispielsweise muss const int value = 12 korrekt sein const int value; value = 12; diese Syntax ist falsch. Schauen wir uns als Nächstes ein etwas schwierigeres Problem an: konstante Zeiger im Vergleich zu Zeigerkonstanten. Schauen wir uns zunächst einen Code an:
#define SWITCH 1 int main() { int val_1 = 5; int val_2 = 10; const int *p1 = &val_1; int const *p2 = &val_1; int *const p3 = &val_1; #ifdef SWITCH // This is a switch *p1 = 20; *p2 = 21; *p3 = 22; #endif #ifndef SWITCH p1 = &val_2; p2 = &val_2; p3 = &val_2; #endif printf("%d\n", *p1); printf("%d\n", *p2); printf("%d\n", *p3); return 0; }
Bei der Ausführung unter dem Cygwin-Compiler können wir diesen Fehler sehen:
Auf dem Bild können wir deutlich erkennen, dass die Zeiger p1 und p2 den Wert in val_1 nur als Zeigerkonstante lesen können, also nicht ändern können worauf es hindeutet. Der Inhalt der Variablen, also *p1 = 20; *p2 = 21; (#ifdef SWITCH ... #endif ist eine bedingte Kompilierung, bei der es sich um einen Makroschalter handelt). Dann kommentieren wir die Anweisung #define SWITCH 1 aus. Zu diesem Zeitpunkt wird der zweite Codeblock ausgeführt und das Ergebnis ist wie folgt:
Es ist aus dem Fehler ersichtlich dass p3 ein konstanter Zeiger ist. Es kann nur auf eine feste Adresse zeigen und die Richtung, auf die es zeigt, nicht ändern. Daher ist die Operation von p3 = &val_2 falsch, daher lautet der richtige Code wie folgt:
int main() { int val_1 = 5; int val_2 = 10; const int *p1 = &val_1; int const *p2 = &val_1; int *const p3 = &val_1; printf("Frist\n"); printf("%d\n", *p1); printf("%d\n", *p2); printf("%d\n", *p3); p1 = &val_2; p2 = &val_2; *p3 = 22; printf("Second\n"); printf("%d\n", *p1); printf("%d\n", *p2); printf("%d\n", *p3); return 0; }
Das Ergebnis der Operation ist:
Endgültig Ende: Konstanter Zeiger (const int *p oder int const *p) bedeutet, dass Zeiger p den in der Adresse gespeicherten Wert nicht ändern kann, aber die Zeigerkonstante (int *const p) bedeutet das Zeiger p kann die Adresse, auf die er zeigt, nicht ändern, das heißt, der Zeiger kann den Ort, auf den er zeigt, nicht ändern, kann aber den Inhalt des Ortes ändern, auf den er zeigt. Wenn Sie möchten, dass der Zeiger weder den Ort, auf den er zeigt, noch den Inhalt dort ändern kann, können Sie ihn wie folgt definieren:
const int * const p = &a;或int const *const p = &a; 在定义函数的时候,若该入口参数在程序执行的过程中不希望被改变,则一定要将该形参用const来修饰,一来这样可以防止该段程序将其改变,二来对于形参而言,一个无论是否是const修饰的实参都可以将其传入const形的形参,而一个const形的实参是无法传入非const形的形参中,所以为了使编译不出错在定义函数的时候,一定要将不希望被改变的量用const关键字来修饰。
Static关键字为静态关键字,它的作用是将作用的变量存入内存中,而非存入寄存器中(即将变量存入堆中而非栈中),并且该作用的变量仅保存最近一次获取的值。接下来我们来看一段代码。
void countfun () { static int count = 0; ++count; printf(“This is %d number, enter into this function !\n”, count ); } int main() { for (int i = 0; i < 5; ++i) { countfun(); } return 0; }
这段代码的运行结果如下:
而若将除去static关键字,则运行的结果如下:
由此我们可以清楚的看出,static作用的变量count只会存入当前的结果,因此循环调用countfun( )函数的时候并没有从新将count变量置为0,而是保存了前一次的值。
Static关键字在项目中的应用是很广泛的,它不仅仅有上述所介绍的特点,同时若想要定义的全局变量在整个工程中仅在当前.C文件中有效时,也应该将这个全局变量用static来修饰,这样在其他的文件中是无法访问这个变量,从而降低了模块间的耦合度,提高了模块的内聚性,防止其他文件将其改变,从而更加的安全。
volatile关键字在嵌入式领域中是十分重要的一个关键字,尤其是在与硬件相关或多线程的编程中更为重要。volatile关键字修饰的变量说明它是可以随时发生改变的,我们不希望编译器去优化某些代码的时候,需要将这个变量用volatile关键字来修饰,从而程序每次访问该变量的时候是直接从内存中提取出来,而不是从临时的寄存器中将该变量的副本给提取出来利用!例如当我们想要实现某个中断处理时,其用来做判断条件的标记位则应该用volatile来修饰,这样当这个中断在别的地方被触发的时候就可以被实时的检测到,不至于由于优化而忽略中断。接下来我们看一段代码:
int main() { volatile int i = 10; int a = i; printf(“i = %d\n”, a); __asm { mov dword ptr[ebp-4], 0x10 } int b = i; printf(“i = %d\n”, b); return 0; }
此程序输出结果为i = 10;i = 16; 若将volatile关键字去掉,则结果为i = 10;i = 10;
即不加关键字会将汇编代码忽略掉,所以为了防止代码优化以及可以及时检测到外部程序对该变量的改变,我们必须将该变量加上volatile关键字。我们知道volatile关键字表征该量是易变的,const关键字代表该量是常量不能改变,那么volatile与const是否可以一起修饰同一个量呢,是肯定的,例如在硬件编程中ROM所存储的数据是不允许用户改变的,即指向该数据的指针必须为常量指针(const int *p = &ram_data),然而开发商却可以将其意外的改变,为了防止ROM的内容被意外的改变时,而用户程序没有及时的发现,必须将该量用volatile修饰,所以应这样定义该指针(volatile const int *p = &rom_data)。
位运算
在数字解码与编码的过程中,位运算的操作是司空见惯的事,同时位运算在提高程序的性能方面也独占鳌头,因此位运算操作是必需要深入了解的问题。
在乘法以及除法的操作中我可以使用未运行来提高代码的质量,例如:a = a * 16;这种操作完全可以替换为:a = a << 4;我们知道左移一位相当于将原数乘以2,左移N位则相当于乘以2^N,前提是在没有发生溢出的情况下;故上例即相当于将数a左移4位,对于某些乘以非2的整数幂情况,如 a = a * 9;则可以改写为a = (a << 3) + a; 同理右移相当于除以2的整数幂,当然以上所有情况都是在没有发生数据溢出的情况下,因此位运算操作要格外的小心,否则极有可能发生出错的情况。
在数据类型转换的过程中也需要做位运算操作,例如我们想将一个unsigned short类型的数据存入unsigned char类型的数组中,就需要进行位运算,首先分析知道unsigned short占用16个字节,unsigned char占用8个字节,想要将大字节的数据存入小字节,必须要对大字节进行分割,即将高8位与低8为分离开来分别存放,来看实现代码:
unsigned char * DrawCompo_Pj_BT_Change(unsigned short *subarray) { unsigned char temp[500]; (void)_sys_memset(&temp, 0x00, sizeof(temp) ); unsigned short i = 0; while (subarray[i] != 0x0000) { if( (subarray[i] & 0xff00) == 0x0000) { temp[i++] = (unsigned char)(subarray[i] & 0x00ff); } else { temp[i] = (unsigned char)( (subarray[i] & 0xff00) >> 8); temp[i++] = (unsigned char)(subarray[i] & 0x00ff); } } temp[i] = '\0'; return temp; }
temp[i] = (unsigned char)( (subarray[i] & 0xff00) >> 8);即取subarray[i]数据的高8位,temp[i++] = (unsigned char)(subarray[i] & 0x00ff);取低8位。这样就可以实现将高字节的数据完整的存入到低字节中。
位运算还可以用来判断变量的符号,我们知道对于一个有符号的变量,其最高位为其符号位,故检查改变的最高位即可知道该变量为正还是为负。看一段代码:
int main() { short test_data = -12; if (test_data & 0xF000) { printf("This number is negative "); } else { printf("This number is positive "); } return 0; }
对于想要交换两个数的值的时候,通常我们的做法如下:
void swap(int &data1, int &data2) { int temp = 0; temp = data1; data1 = data2; data2 = temp; }
这样的代码比较简单易懂,然而美中不足的是它会产生一个临时变量temp,接下来我们用位运算来重写这个程序;
void swap(int &data1, int &data2) { data1 = data1 ^ data2; data2 = data1 ^ data2; data1 = data1 ^ data2; }
从上面的代码我们可以看出少了一个临时变量,同时也加快了代码的运行效率。
尾递归:
递归调用给我们带来了很多方便,也简化了代码,使程序看起来更加的简洁和明了,但递归调用也通常伴随着一个潜在的危险:出栈,接下来我们来看一个常见的递归。
int factorial(int n) { if (n < 1) { return 1; } else { return factorial(n-1)*n; } }
通常在求一个数的阶乘的时候我们习惯于采用上述方法,然而分析来看当输入的n值较大时,factorial(n-1)*n所计算的值会不断的压入堆栈,生成很多的临时变量,等待下一个的值的确定才得以计算,然而在内存中堆栈的大小是固定的,当输入的n值很大时,极有可能产生堆栈溢出!因此,有一个好的方法解决这种问题即尾递归调用。接下来我们来看这种强大的算法。
int factorial(int n, int m) { if (n < 2) { return m; } else { factorial(n-1, n*m); } }
从代码中可以看出,通过引入一个新的参数m来存放每次递归所产生的值,这样就避免了每次递归都要进行压栈操作,也就不会产生堆栈溢出的现象,而且普通的递归每次递归只能等待下一次递归得到的结果后才能继续运算,而尾递归每次执行都会进行运算,一次循环执行完毕即可得到结果,其时间复杂度为O(n);