*** ВНИМАНИЕ: Блог переехал на другой адрес - demin.ws ***
Показаны сообщения с ярлыком c++. Показать все сообщения
Показаны сообщения с ярлыком c++. Показать все сообщения

воскресенье, 22 мая 2011 г.

Индексация по строковой константе

Признаюсь, мне никогда раньше не приходило в голову индексировать строковую константу прямо на месте. Например:

#include <stdio.h>

int main() {
  int i;
  for (i = 0; i < 8; ++i)
    printf("%c", "12345678"[i]);
  printf("\n");
  return 0;
}

Лично мне выражение "12345678"[i] как-то режет глаз. Хотя с точки зрения языка тут все в порядке.

вторник, 18 января 2011 г.

Компилятор языка С: LCC-WIN32

Потребовалось мне тут прикрутить к замечательной программе putty подсветку синтаксиса на лету в терминальной сессии. И так вышло, что на новом рабочем ноутбуке я пока еще не успел поставить Студию. У putty есть makefile’ы для Visual Studio, Borland’а, Cygwin’а и LCC. Первых двух у меня не было, и взять их было негде, Cygwin недолюбливаю из-за необходимости таскать с собой потом dll’ки, и чудом выбор пал на LCC. До этого я никогда этот компилятор не использовал.

И я был приятно удивлен практически всему увиденному. Во всего шести мегабайтах дистрибутива вы получаете быстрый компилятор С99 с поддержкой современных процессоров, линковщик, ассемблер, компилятор ресурсов и внушительную библиотеку.

Про библиотеку хочу сказать отдельно. Помимо стандартного набора libc и Win32 API, там полно всего остального. Лично я был несказанно удивлен простой, и порой столь нужной функцией ping() (и не надо больше вызывать ping.exe в скрытом окне).

В общем, с помощью также идущих в комплекте регулярных выражений, я быстро подхачил putty как мне было нужно. Попутно проронил ностальгическую слезу от программирования оконного интерфейса на чистом Win32 API и ощутил некоторые приятности С99. Например, объявление переменных не в начале блока, а где удобно, и размер автоматических массивов задавать не статически, а из переменной. C99 однозначно стоит внимательного изучения.

Приведу небольшую выжимку из идущих в комплекте библиотек (кроме стандартных libc и Windows API, конечно). Думаю, названия говорят сами за себя.

gl.h OpenGL
sqlite.h  
bignums.h Работа с числами произвольной точности
bitstring.h  
bluetoothapis.h  
d3d.h  
d3dx.h  
dynloader.h Работа с DLL’ками
gc.h Сборщик мусора (требует запуска, конечно)
getopt.h  
icmpapi.h  
int128.h  
matrix.h Работа с векторами и матрицами
mq.h IBM MQ
msi.h  
netmon.h  
netsh.h  
pcre.h Регулярные выражения в стиле Perl
ping.h PING!
ras.h  
regexp.h Простой API для регулярных выражений (regcomp() и regexec())
snmp.h  
sqlite3.h  
str.h Работа со строками в стиле C99
tapi.h  

Итак, если вам быстро нужен небольшой компилятор (дистрибутив всего шесть мегабайт), для написания программы на С99 под Windows (для графического интерфейса придется все делать на чистом Win32 API), имеющий в комплекте в дополнение к libc и Win32 API приличный набор разнообразных библиотек, то LCC – это очень сильный кандидат.

Кстати, отдельно можно скачать и 64-битную версию компилятора.

Единственное, чего я не пробовал – это линковать объектники LCC с другими компиляторами. Кто имеет опыт – поделитесь.

суббота, 25 сентября 2010 г.

Функции, указатели на них и оператор "?"

Был удивлен, когда компилятор С съел вот такой забавный способ условного вызова функций:

#include <stdio.h>
#include <math.h>
int main() {
  int i;
  for (i = 0; i <= 1; ++i) {
    float a = (i ? floor : ceil) (10.5);
    printf("%d: %f\n", i, a);
  }
  return 0;
}

Для С++ надо написать:

#include <stdio.h>
#include <cmath>
int main() {
  int i;
  typedef float (*f)(float);
  for (i = 0; i <= 1; ++i) {
    float a = (i ? (f)std::floor : (f)std::ceil) (10.5);
    printf("%d: %f\n", i, a);
  }
  return 0;
}

или

#include <stdio.h>
#include <cmath>
int main() {
  int i;
  for (i = 0; i <= 1; ++i) {
    float a = (i ? std::floorl : std::ceill) (10.5);
    printf("%d: %f\n", i, a);
  }
  return 0;
}

Все программы выводят:

0: 11.000000
1: 10.000000

суббота, 6 марта 2010 г.

Решето Эратосфена - кто быстрее: Go, C или C++?

Go очень интересный язык. Компиляция в native-code (никаких виртуальных машин, JIT-компиляций и т.д.), при этом автоматическая сборка мусора и встроенная поддержка многопоточности, объектно-ориентированная модель, и в довершение всего - очень быстрая компиляция.

Лично я обычно на новых для меня языках люблю писать Решето Эратосфена в качестве "Hello, world!".

Моя версия на Go выгдядит так:

Файл erato-go-bool.go:

package main

import "fmt"
import "math"
import "flag"

func main() {
    var N int
    flag.IntVar(&N, "N", 100, "")
    flag.Parse()

    fmt.Printf("%d\n", N)

    seive := make([]bool, N)
  
    limit := int(math.Sqrt(float64(N))) + 1

    for i := 2; i < limit; i++ {
        if !seive[i] {
            for j := i * i; j < N; j += i  {
                seive[j] = true
            }
        }
    }

    count := 0
    for i := 2; i < N; i++ {
        if !seive[i] {
            count++
        }
    }
    fmt.Printf("%d\n", count)
}

И первый вопрос, который приходит в голову - а насколько это быстро работает?

Некоторое время назад я уже писал, как использовал решето для тестирования STL'евского контейнера std::vector на разных компиляторах.

Сейчас я провел похожее сравнение между Go, C++ и C.

Итак, первый кандитат - версия на Go с использованием типа bool (см. выше). Второй - тоже на Go, но с использованием типа int.

Файл erato-go-int.go:

package main

import "fmt"
import "math"
import "flag"

func main() {
    var N int
    flag.IntVar(&N, "N", 100, "")
    flag.Parse()

    fmt.Printf("%d\n", N)

    seive := make([]int, N)
  
    limit := int(math.Sqrt(float64(N))) + 1

    for i := 2; i < limit; i++ {
        if seive[i] == 0 {
            for j := i * i; j < N; j += i  {
                seive[j] = 1
            }
        }
    }

    count := 0
    for i := 2; i < N; i++ {
        if seive[i] == 0 {
            count++
        }
    }
    fmt.Printf("%d\n", count)
}

Далее идет версия на С++. Макрос TYPE позволяет переключать программу для нужно типа в контейнере (int или bool):

Файл erato-cxx.cpp:

#include <iostream>
#include <vector>
#include <cstdlib>
#include <cmath>

int main(int argc, char* argv[]) {
  int n = argc > 1 ? std::atoi(argv[1]) : 100;

  std::cout << n << std::endl;

  int sqrt_n = static_cast<int>(std::sqrt(static_cast<double>(n))) + 1;

  std::vector<TYPE> S(n, true);

  for (int i = 2; i < sqrt_n; ++i)
    if (S[i])
      for (int j = i*i; j < n; j+=i)
        S[j] = false;

  int count = 0;
  for (int i = 2; i < n; ++i)
    if (S[i])
      count++;

  std::cout << count << std::endl;

  return 0;
}

Ну и для полноты картины версия на С:

Файл erator-c-int.c:

#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <math.h>

int main(int argc, char* argv[]) {
  int n = argc > 1 ? atoi(argv[1]) : 100;
  int* S;
  int count;
  int sz = n * sizeof(*S);
  int i, j;

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

  long sqrt_n = sqrt(n) + 1;

  S = malloc(sz);
  memset(S, 0, sz);

  for (i = 2; i < sqrt_n; ++i)
    if (S[i] == 0)
      for (j = i*i; j < n; j+=i)
        S[j] = 1;

  count = 0;
  for (i = 2; i < n; ++i)
    if (S[i] == 0)
      count++;

  printf("%d\n", count);

  free(S);
  return 0;
}

Ну и Makefile для удобного запуска.

Файл Makefile:

.SILENT: 

all:
        $(MAKE) run 2>&1 | tee log
        $(MAKE) parse-log

run: go-bool go-int cxx-int cxx-bool c-int

N ?= 100000000

go-bool:
        echo $@
        6g erato-$@.go
        6l -o erato-$@ erato-$@.6
        time -p -f %e ./erato-$@ -N=$(N)

go-int:
        echo $@
        6g erato-$@.go
        6l -o erato-$@ erato-$@.6
        time -p -f %e ./erato-$@ -N=$(N)

cxx-bool:
        echo $@
        g++ -o erato-$@ \
                -O3 -funroll-all-loops -fomit-frame-pointer \
                -DTYPE=bool erato-cxx.cpp
        time -p -f %e ./erato-$@ $(N)

cxx-int:
        echo $@
        g++ -o erato-$@ \
                -O3 -funroll-all-loops -fomit-frame-pointer \
                -DTYPE=int erato-cxx.cpp
        time -p -f %e ./erato-$@ $(N)

c-int:
        echo $@
        gcc -o erato-$@ -lm \
                 -O3 -funroll-all-loops -fomit-frame-pointer erato-$@.c
        time -p -f %e ./erato-$@ $(N)

parse-log:
        printf "%10s %10s %8s %5s\n" "Language" N Count Time ; \
        (echo "------------------------------------") ; \
        while read type ; do \
                read N && \
                read count && \
                read time && \
                printf "%10s %10s %8s %5s\n" $$type $$N $$count $$time ; \
        done < log

Запускал я все это под Ubuntu 64-bit. Компилятор C и C++ - gcc 4.4.1. Компилятор Go - последний из официального репозитория.

Запускаем:

make N=100000000

и получаем следующее:

 Language           N    Count  Time
------------------------------------
   go-bool  100000000  5761455  3.96
    go-int  100000000  5761455  6.58
   cxx-int  100000000  5761455  6.76
  cxx-bool  100000000  5761455  2.20
     c-int  100000000  5761455  6.47

Получается, что сделал всех С++ с использованием std::vector<bool> для хранения массива. Затем идет Go тоже с типом bool. А С, С++ с std::vector<int> и Go с int'ом примерно равны.

Update: После экспериментов в комментариях выходит, что и на С, и на С++ можно добиться равного быстродействия, если использовать битовые поля. Просто в С++ это синтаксически проще, но не более того.

Посты по теме:

понедельник, 21 сентября 2009 г.

Двойная точка с запятой в разделе объявления переменных

Казалось бы, невинный пример (vs_double_semicolumn.c):
void main() {
int a;;
int b;
}
Компилируем (в режиме языка С, то есть без /TP):
cl vs_double_semicolumn.c
Результат:
vs_double_semicolumn.c
vs_double_semicolumn.c(3) : error C2143: syntax error : missing ';' before 'type'
Результат в Codegear/Borland примерно такой же (хотя описание ошибки более ясное):
CodeGear C++ 5.93 for Win32 Copyright (c) 1993, 2007 CodeGear
vs_double_semicolumn.c:
Error E2140 vs_double_semicolumn.c 3: Declaration is not allowed here in function main
*** 1 errors in Compile ***
Проблемка заключается в случайной опечатке в виде двойного символа ;. Кстати, пример абсолютно реальный, из жизни. Случайная опечатка - и сразу много вопросов.

Получается, что второй символ ; тут трактуется как пустой оператор, а не пустая декларация переменной. Компилятор решает, что объявления переменных закончены, и начался блок операторов, поэтому резонно ругается на попытку объявить переменную b там, где уже должны быть операторы.

Проверил на gcc, на родных компиляторах AIX, Solaris и HP-UX. Эти все съели пример без проблем.

Посты по теме:

понедельник, 9 марта 2009 г.

sizeof('a') в С и C++

В очередной раз узнал для себя новый потенциальный вопрос для "прогиба" на каком-нибудь тесте по языку С.

Программа x.c:
#include <stdio.h>
int main() {
char s[4];
s[0] = 'C'; s[1] = s[2] = '+'; s[3] = 0;
s[sizeof(' ') ^ 5] = 0;
printf("%s\n", s);
return 0;
}
Компилируем и запускаем.

Visual Studio:
cl x.c && x
или в Cygwin:
gcc -o x x.c && x
Имеем следующий результат:
C
А теперь так:
cl /TP x.c && x
или в Cygwin:
g++ -o x x.c && x
Теперь программа печает иное:
C++
Результат повеселил некоторых моих коллег.

Нашел небольшой список еще некоторых "отличий" С и С++, но, пожалуй, этот самый неявный, а значит потенциально опасный.

воскресенье, 1 марта 2009 г.

Unit-тестирование в языке С

По роду работы мне приходится работать с огромным количеством кода на С, причем чаще всего — это старый код, написанный много лет назад, и написан он без каких-либо намеков на тестирование, увы.

Исправляя в таком коде ошибки, внося какие-то изменения, хочется какой-то гармонии с самим собой, а именно — иметь возможность тестировать, тем самым уменьшить вероятность повторного внесения ошибок. Пусть уж полностью старый код остается без тестов, но раз уж я что-то меняю, я хочу подкрепить свои изменения тестами.

Мир языка С++ не такой дружественный к тестированию, как например, мир Java, C# или мир интерпретаторов. Главная причина — крайне слабый механизм интроспекции, то есть возможности исследования двоичного кода в плане получения информации о структуре исходных текстов. В Java, например, есть "The Reflection API", с помощью которого можно прямо на основе скомпилированных классов создать тестовую среду (понять иерархию классов, типа аргументов и т.д.). В С++ приходится многое закладывать в исходный текст на этапе его создания, чтобы облегчить будущее тестирование.

А что же мы имеем в С? Тут, как мне кажется, разрыв в удобстве тестирования по отношению к С++ в разы больше, чем между С++ и Java, например. Причин море: процедурная модель вместо объектно-ориентированной, отсутствие интроспекции вообще, крайне слабая защита при работе с памятью и т.д.

Но шансы все же остались. Я начал поиск готовых библиотек для unit-тестирования в С. Например, есть библиотека MinUnit, длиной в четыре строки. Вполне жизненно. Следующий вполне себе вариант — это CUnit. Тут даже есть продвинутый консольный интерфейс.

Перебрав еще несколько вариантов, я остановился на гугловской библиотеке cmockery. Мне понравилось, что библиотека, несмотря на весьма сложный код, успешно компилируются не только в Visual Studio и GNU C, но и “родными” компиляторами AIX, HP-UX, SunOS и некоторых других экзотических зверей. Также библиотека умеет отлавливать утечки памяти, неправильную работу с распределенными кусками памяти (так называемые buffer over- и under- run). Еще в cmockery есть зачатки mock-механизмов, то есть когда задаются предполагаемые сценарии выполнения тестируемого блока, и потом результаты тестового прогона сверяются с предполагаемым сценарием. Mock-возможности я не буду пока рассматривать в данной статье. Про это стоит написать отдельно.

На текущий момент актуальной версией cmockery является 0.1.2. Из всего архива реально нужны только два файла: cmockery.c и cmockery.h. Можно, конечно, собрать библиотеку как положено, в двоичном виде, но я предпочитаю работать всегда с исходными текстами, благо компилируется очень быстро (это ж не С++).

Желающие, могут скачать мою сборку cmockery. В этом архиве только необходимые два файла cmockery.c и cmockery.h. Также в файл cmockery.h я внес небольшое изменение, связанное к тем, что функция IsDebuggerPresent() почему-то явно объявлена в заголовочных файлах только в Visual Studio 2008. Для студии 2003 и 2005 надо вручную объявлять прототип, иначе при линковке вылезает сообщение:

error LNK2019: unresolved external symbol _IsDebuggerPresent referenced in function __run_test

Я отрапортовал об этом досадном недочете авторам, и пока новый релиз cmockery не вышел, можно пользоваться моей сборкой, которая без предупреждений компилируются в любой студии.

Теперь пример реального использования cmockery.

Я долго выбирал то, на чем можно хоть как-то наглядно продемонстрировать unit-тестирование в С. В итоге я остановился на библиотеке для работы со строками. Эта библиотека реализует так называемые строки с длинной. То есть надо для кода на С дать более менее удобный интерфейс для манипулированию строками, которые хранят внутри себя длину.

Основа библиотеки была написана весьма давно, и много раз переписывалась практически с нуля, но я все еще использую ее в некоторых проектах.

Естественно, я не буду приводить всю библиотеку. Во-первых, она весьма тривиальна и вся ее "фишка" состоит в удобности работы, нежели в какой-то особо хитрой и заумной реализации. Во-вторых, полный ее исходный текст весьма объемен. Я выбрал небольшой ее фрагмент, но его тестирование позволяет почувствовать дух тестирования в С.

Итак, библиотека cstring. Тут можно создавать в некоторые "объекты", реализованные через структуры, которые представляют собой "строки". Такая "строка" может создаваться либо в стеке (автоматическая переменная), либо в куче. Также предоставляется набор разнообразных базовых функций: определение длины, копирование, склейка, интерфейс со строками языка С (char *) и т.д. Как я уже сказал, для демонстрации системы тестирования я оставил только несколько функций.

Заголовочный файл cstring.h:

#ifndef _CSTRING_H
#define _CSTRING_H

#define _decl_string_t(N) \
struct { \
int sz; \
char data[N]; \
}

typedef _decl_string_t(1) string_t;

/**
* Объявление строки в форме автоматической переменной в стеке.
* Длина строки инициализируется нулем.
*/
#define decl_string_t(name, size) _decl_string_t(size) name = { 0 }

/**
* Создание новой строки в куче.
*/
string_t* string_new(int sz);

/* Трюк с дублированием имен функций, начинающихся с символа '_'
* требуется для подавление предупреждений компилятора о преобразовании
* типов.
*/

/**
* Удаление строки из кучи.
*/
#define string_delete(str) _string_delete((string_t*)str)
void _string_delete(string_t* str);

/**
* Текущая длина строки.
*/
#define string_length(str) _string_length((const string_t*)str)
int _string_length(const string_t* str);

/**
* Изменение длины строки.
*/
#define string_resize(str, sz) _string_resize((string_t*)str, sz)
int _string_resize(string_t* str, int sz);

/**
* Копирование строки из строки С, завершающейся нулем.
*/
#define string_from_c_str(dst, src) _string_from_c_str((string_t*)dst, src)
string_t* _string_from_c_str(string_t* dst, const char* src);

/**
* Добавление символа в строку.
*/
#define string_append_ch(str, ch) _string_append_ch((string_t*)str, ch)
string_t* _string_append_ch(string_t* str, char ch);

/**
* Превращение строки в строку С без добавления нуля на конце.
*/
#define string_data(str) str->data

/**
* Превращение строки в строку С с нулем на конце.
*/
#define string_c_str(str) _string_c_str((string_t*)str)
char* _string_c_str(string_t* str);

#endif
Файл cstring.c:
#include <stdlib.h>

#include "cstring.h"

/**
* Подготовительная площадка для тестирования.
* Если задан макрос UNIT_TESTING, то функции работы с кучей подменяются
* на тестовые.
*/
#if UNIT_TESTING
extern void* _test_malloc(const size_t size, const char* file, const int line);
extern void* _test_calloc(const size_t number_of_elements, const size_t size,
const char* file, const int line);
extern void _test_free(void* const ptr, const char* file, const int line);

#define malloc(size) _test_malloc(size, __FILE__, __LINE__)
#define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__)
#define free(ptr) _test_free(ptr, __FILE__, __LINE__)
#endif // UNIT_TESTING

/**
* Создание новой строки в куче. Трюк "sizeof(string_t)" используется, чтобы
* правильно отработать ситуацию, если из-за выравнивания между элементами
* структуры string_t 'sz' и 'data' вдруг появится промежуток.
*/
string_t* string_new(int sz) {
return malloc(sizeof(string_t) + sz - 1);
}

/**
* Удаление строки из кучи.
*/
void _string_delete(string_t* str) {
free((void *)str);
}

/**
* Текущая длина строки.
*/
int _string_length(const string_t* str) {
return str->sz;
}

/**
* Изменение длины строки.
*/
int _string_resize(string_t* str, int sz) {
return str->sz = sz;
}

/**
* Копирование строки из строки С, завершающейся нулем.
*/
string_t* _string_from_c_str(string_t* dst, const char* src) {
int sz = strlen(src);
memcpy(dst->data, src, sz);
dst->sz = sz;
return dst;
}

/**
* Добавление символа в строку.
*/
string_t* _string_append_ch(string_t* str, char ch) {
str->data[str->sz++] = ch;
return str;
}

/**
* Превращение строки в строку С с нулем на конце. Фактически,
* в тело строки добавляется ноль и возвращается указатель на данные.
*/
char* _string_c_str(string_t* str) {
str->data[str->sz] = 0;
return string_data(str);
}
Как вы заметили, в коде есть специальный блок, ограниченный макросом UNIT_TESTING. Ничего не поделаешь, в языке С приходится "готовить" код к потенциальному тестированию и вставлять фрагменты, позволяющие тестовой среде работать с этим кодом. Этот блок, если задан макрос UNIT_TESTING, переопределяет функции работы с кучей, чтобы можно было перехватывать их вызовы. Подменяющие функции _test_malloc(), _test_calloc() и _test_free() предоставляются библиотекой cmockery.

Теперь файл тестов cstring_unittest.c:

#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmockery.h>

#include "cstring.h"

/**
* Тестируем декларацию строки длиной 20 в виде автоматической
* переменной, добавляем в нее два символа, обрезаем строку
* до длины в один байт и проверяем, добавился ли 0 при преобразовании
* в строку С.
*/
void string_c_str_test(void **state) {
decl_string_t(a, 20);
a.data[0] = 'a';
a.data[1] = 'b';
a.sz = 1;
assert_memory_equal("a\0", string_c_str(&a), 2);
}

/**
* Тестируем изменение длины строки.
*/
void string_resize_test(void **state) {
decl_string_t(a, 20);
a.sz = 2;
string_resize(&a, 1);
assert_int_equal(1, string_length(&a));
}

/**
* Тестируем добавление символа путем сравнения со строками С
*/
void string_append_ch_test(void **state) {
decl_string_t(a, 20);
assert_string_equal("", string_c_str(&a));
assert_string_equal("a", string_c_str(string_append_ch(&a, 'a')));
assert_string_equal("ab", string_c_str(string_append_ch(&a, 'b')));
}

/**
* Тестируем декларацию строки в виде автоматической переменной.
* Длина строки сразу после декларации должна быть нулевой.
*/
void string_declare_test(void **state) {
decl_string_t(a, 20);
assert_int_equal(0, string_length(&a));
}

/**
* Тестируем размещение новой строки в куче и ее удаление из нее.
*/
void string_heap_allocation_test(void **state) {
string_t* a = string_new(20);
string_delete(a);
}

/**
* Тестируем копирование строки из строки С с нулем на конце.
*/
void string_from_c_str_test(void **state) {
string_t* a = string_new(8);
string_from_c_str(a, "12345678");
assert_int_equal(8, string_length(a));
string_delete(a);
}

/**
* Создаем список тестов и запускаем их.
*/
int main(int argc, char* argv[]) {
const UnitTest tests[] = {
unit_test(string_declare_test),
unit_test(string_c_str_test),
unit_test(string_append_ch_test),
unit_test(string_heap_allocation_test),
unit_test(string_from_c_str_test),
unit_test(string_resize_test),
};
return run_tests(tests);
}
Схема очень похожа на любое другое xUnit тестирование: каждый тест проверяет какой-то один функциональный элемент, тесты объединяются в группы и запускаются автоматически все вместе. Правда, из-за ограничений языка С каждый тест приходится вручную добавлять в список запуска, увы.

Как я уже сказал, для компиляции потребуются файлы cmockery.c и cmockery.h (см. выше). Эти файлы можно положить в текущий каталог.

Компилируем в Visual Studio:

cl /DUNIT_TESTING /I. cstring_unittest.c cstring.c cmockery.c
Если все скомпилировалось нормально, то запускаем файл cstring_unittest:
string_declare_test: Starting test
string_declare_test: Test completed successfully.
string_c_str_test: Starting test
string_c_str_test: Test completed successfully.
string_append_ch_test: Starting test
string_append_ch_test: Test completed successfully.
string_heap_allocation_test: Starting test
string_heap_allocation_test: Test completed successfully.
string_from_c_str_test: Starting test
string_from_c_str_test: Test completed successfully.
string_resize_test: Starting test
string_resize_test: Test completed successfully.
All 6 tests passed
Все тесты отработали правильно.

Но неинтересно, когда все работает. Внесем в тест библиотеки "случайные ошибки". Каждую из них можно спокойно допустить непреднамеренно. Строки с ошибками я пометил комментариями со словом "ОШИБКА (!)". Посмотрим, как cmockery справится с этим.

Файл cstring.c с "ошибками":

#include <stdlib.h>

#include "cstring.h"

/**
* Подготовительная площадка для тестирования.
* Если задан макрос UNIT_TESTING, то функции работы с кучей подменяются
* на тестовые.
*/
#if UNIT_TESTING
extern void* _test_malloc(const size_t size, const char* file, const int line);
extern void* _test_calloc(const size_t number_of_elements, const size_t size,
const char* file, const int line);
extern void _test_free(void* const ptr, const char* file, const int line);

#define malloc(size) _test_malloc(size, __FILE__, __LINE__)
#define calloc(num, size) _test_calloc(num, size, __FILE__, __LINE__)
#define free(ptr) _test_free(ptr, __FILE__, __LINE__)
#endif // UNIT_TESTING

/**
* Создание новой строки в куче. Трюк "sizeof(string_t)" используется, чтобы
* правильно отработать ситуацию, если из-за выравнивания между элементами
* структуры string_t 'sz' и 'data' вдруг появится промежуток.
*/
string_t* string_new(int sz) {
return malloc(sizeof(string_t) + 1 - 1); // (ОШИБКА!) "Неверная" длина.
}

/**
* Удаление строки из кучи.
*/
void _string_delete(string_t* str) {
// (ОШИБКА!) "Забыли" вызвать free().
}

/**
* Текущая длина строки.
*/
int _string_length(const string_t* str) {
return str->sz;
}

/**
* Изменение длины строки.
*/
int _string_resize(string_t* str, int sz) {
return str->sz; // (ОШИБКА!) "Забыли" уменьшить длину строки.
}

/**
* Копирование строки из строки С, завершающейся нулем.
*/
string_t* _string_from_c_str(string_t* dst, const char* src) {
int sz = strlen(src);
memcpy(dst->data, src, sz);
// (ОШИБКА!) "Забыли" присвоить длине новое значение.
return dst;
}

/**
* Добавление символа в строку.
*/
string_t* _string_append_ch(string_t* str, char ch) {
str->data[str->sz] = ch; // (ОШИБКА!) "Забыли" увеличить длину.
return str;
}

/**
* Превращение строки в строку С с нулем на конце. Фактически,
* в тело строки добавляется ноль и возвращается указатель на данные.
*/
char* _string_c_str(string_t* str) {
// (ОШИБКА!) "Забыли" добавить 0 в конец.
return string_data(str);
}
Компилируем и запускаем:
string_declare_test: Starting test
string_declare_test: Test completed successfully.
string_c_str_test: Starting test
difference at offset 1 0x00 0x62
1 bytes of 0x0040f014 and 0x0012fe7c differ
ERROR: cstring_unittest.c:19 Failure!
string_c_str_test: Test failed.
string_append_ch_test: Starting test
"ab" != "b"
ERROR: cstring_unittest.c:39 Failure!
string_append_ch_test: Test failed.
string_heap_allocation_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
ERROR: string_heap_allocation_test leaked 1 block(s)
string_heap_allocation_test: Test failed.
string_from_c_str_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!
string_from_c_str_test: Test failed.
string_resize_test: Starting test
0x1 != 0x2
ERROR: cstring_unittest.c:29 Failure!
string_resize_test: Test failed.
5 out of 6 tests failed!
string_c_str_test
string_append_ch_test
string_heap_allocation_test
string_from_c_str_test
string_resize_test
Blocks allocated...
0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!
Бам! 5 из 6 тестов сломаны. Проанализируем полученное.

Тест string_c_str_test выявил, что функция string_c_str не добавила 0 в конец строки, хотя должна была:

string_c_str_test: Starting test
difference at offset 1 0x00 0x62
1 bytes of 0x0040f014 and 0x0012fe7c differ
ERROR: cstring_unittest.c:19 Failure!
string_c_str_test: Test failed.
Тест string_append_ch_test выявил, что функция добавления символа в конец строки не работает:
string_append_ch_test: Starting test
"ab" != "b"
ERROR: cstring_unittest.c:39 Failure!
string_append_ch_test: Test failed.
Тест string_heap_allocation_test выявил, что у нас имеется неосвобожденный блок памяти (утечка?). Конечно, мы же "забыли" освободить память в функции string_delete():
string_heap_allocation_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
ERROR: string_heap_allocation_test leaked 1 block(s)
string_heap_allocation_test: Test failed.
Тест string_from_c_str_test выявил, что мы "вылезли" за границы выделенного куска памяти. Мы записали что-то мимо. Это болезненная ошибка. Конечно, cmockery не всегда может находить такие ляпы. Например, если переменная выделена с стеке, а не в куче, то проблема не вскроется. Тут уже помогут только динамические отладчики типа valgrind:
string_from_c_str_test: Starting test
Blocks allocated...
0x00326ee0 : cstring.c:27
Guard block of 0x00326f18 size=8 allocated by cstring.c:27 at 0x00326f20 is corrupt
ERROR: cmockery.c:1379 Failure!
string_from_c_str_test: Test failed.
Тест string_resize_test показал, что функция изменения размера строки не работает как положено:
string_resize_test: Starting test
0x1 != 0x2
ERROR: cstring_unittest.c:29 Failure!
string_resize_test: Test failed.
В целом, очень неплохие результаты.
Теперь представьте, что вы решили переписать реализацию библиотеки под новый процессор, чтобы работало в десять раз быстрее. Но как проверить результат? Элементарно. Запустите старые тесты. Если они работают, то по крайней мере с большой вероятностью вы не сломали старую функциональность. И, кстати, чем более тщательно написаны тесты, тем более ценны они. Чем более критична какая часть системы для стабильности системы в целом (например, библиотека строк или каких-то базовых контейнеров), тем более тщательно они должны быть покрыты тестами.
Конечно, уровень комфорта при написании тестов на С и их отладке очень далек даже от С++, но это не может быть оправданием для отказа от тестирования. Честно могу сказать, часто результатом работы "сломанного" теста в С, который неверно работает с памятью, например, может является просто зависание, а не красивый отчет, что тест "не работает". Но даже такой "знак" очень важен и дает понять, что что-то сломано. Пусть лучше повиснет тест, нежели готовый продукт у заказчика.

Под занавес приведу список основных функций-проверок (assert-фукнции), которые доступны в cmockery:

  • assert_true(), assert_false() — проверка булевых флагов
  • assert_int_equal(), assert_int_not_equal() — сравнение для типа int
  • assert_string_equal(), assert_string_not_equal() — сравнение для типа char* (для С-строк, заканчивающихся нулем)
  • assert_memory_equal(), assert_memory_not_equal() — сравнение кусков памяти
  • assert_in_range(), assert_not_in_range() — проверка нахождения числа в указанном интервале
  • assert_in_set(), assert_not_in_set() — проверка нахождения строки (char*) среди заданного набора строк
  • fail() — безусловное завершения теста с ошибкой
Вывод

Unit-тестирование в С порой сопряжено с трудностями, но оно возможно. И нет причин от него отказываться.